JairoDanielMT commited on
Commit
f0e9d8b
verified
1 Parent(s): c0400f5

Upload 4 files

Browse files
Files changed (4) hide show
  1. finanzas.db +0 -0
  2. main.py +382 -0
  3. requirements.txt +7 -0
  4. templates/index.html +185 -0
finanzas.db ADDED
Binary file (24.6 kB). View file
 
main.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import create_engine, Column, Integer, String, Float, ForeignKey, Enum
2
+ from sqlalchemy.orm import sessionmaker, relationship, declarative_base
3
+ from datetime import datetime
4
+ import pytz
5
+ import os
6
+ import requests
7
+ from dotenv import load_dotenv
8
+ from pydantic import BaseModel
9
+ from typing import List
10
+ from flask import Flask, jsonify, render_template
11
+ from flask import request, redirect, url_for, flash
12
+ from flask_cors import CORS
13
+
14
+ app = Flask(__name__)
15
+ app.secret_key = "Jairo_y_Diana"
16
+
17
+ CORS(app)
18
+
19
+
20
+ @app.after_request
21
+ def add_cors_headers(response):
22
+ response.headers["Access-Control-Allow-Origin"] = "*"
23
+ response.headers["Access-Control-Allow-Headers"] = "*"
24
+ response.headers["Access-Control-Allow-Methods"] = "*"
25
+ response.headers["Access-Control-Allow-Credentials"] = "true"
26
+ return response
27
+
28
+
29
+ # Cargar variables de entorno
30
+ load_dotenv()
31
+
32
+ # URL de la API de Google Apps Script
33
+ GOOGLE_SHEET_API_URL = os.getenv("GOOGLE_SHEET_API_URL")
34
+
35
+ # Conexi贸n con la base de datos SQLite (Finanzas.db)
36
+ DATABASE_URL = "sqlite:///finanzas.db"
37
+ engine = create_engine(DATABASE_URL)
38
+
39
+ # Crear una clase base para el ORM
40
+ Base = declarative_base()
41
+
42
+ # Definici贸n de modelos ORM
43
+
44
+
45
+ class Usuario(Base):
46
+ __tablename__ = "usuarios"
47
+
48
+ id_usuario = Column(Integer, primary_key=True, index=True)
49
+ nombre = Column(String, nullable=False)
50
+
51
+
52
+ class Categoria(Base):
53
+ __tablename__ = "categorias"
54
+
55
+ id_categoria = Column(Integer, primary_key=True, autoincrement=True)
56
+ nombre_categoria = Column(String, nullable=False)
57
+ tipo = Column(Enum("Ingreso", "Gasto", name="tipo_categoria"), nullable=False)
58
+
59
+
60
+ class Subcategoria(Base):
61
+ __tablename__ = "subcategorias"
62
+
63
+ id_subcategoria = Column(Integer, primary_key=True, autoincrement=True)
64
+ id_categoria = Column(
65
+ Integer, ForeignKey("categorias.id_categoria"), nullable=False
66
+ )
67
+ nombre_subcategoria = Column(String, nullable=False)
68
+
69
+ categoria = relationship("Categoria", backref="subcategorias")
70
+
71
+
72
+ class Transaccion(Base):
73
+ __tablename__ = "transacciones"
74
+
75
+ id_transaccion = Column(Integer, primary_key=True, autoincrement=True)
76
+ fecha = Column(String, nullable=False)
77
+ tipo = Column(Enum("Ingreso", "Gasto", name="tipo_transaccion"), nullable=False)
78
+ monto = Column(Float, nullable=False)
79
+ id_subcategoria = Column(
80
+ Integer, ForeignKey("subcategorias.id_subcategoria"), nullable=False
81
+ )
82
+ descripcion = Column(String)
83
+ id_usuario = Column(Integer, ForeignKey("usuarios.id_usuario"), nullable=False)
84
+
85
+ subcategoria = relationship("Subcategoria", backref="transacciones")
86
+ usuario = relationship("Usuario", backref="transacciones")
87
+
88
+
89
+ # Funci贸n para obtener la fecha y hora precisa en la zona horaria de Per煤
90
+ def get_peru_time():
91
+ peru_tz = pytz.timezone("America/Lima")
92
+ return datetime.now(peru_tz).strftime("%Y-%m-%d %H:%M:%S")
93
+
94
+
95
+ # Funci贸n para enviar datos a la hoja de Google Sheets
96
+ def send_to_google_sheet(data_list):
97
+ data_dicts = []
98
+
99
+ for data in data_list:
100
+ # Convertir la fecha a un string en el formato adecuado
101
+ data_dict = data.model_dump()
102
+ if isinstance(data_dict["fecha"], datetime):
103
+ data_dict["fecha"] = data_dict["fecha"].strftime("%Y-%m-%d %H:%M:%S")
104
+
105
+ data_dicts.append(data_dict)
106
+ response = requests.post(GOOGLE_SHEET_API_URL, json=data_dicts)
107
+ if response.status_code == 200:
108
+ print("Datos enviados con 茅xito a la Google Sheet.")
109
+ else:
110
+ print(f"Error al enviar datos: {response.status_code}, {response.text}")
111
+
112
+
113
+ # def send_to_google_sheet(data: List[BaseModel]):
114
+ # """
115
+ # Env铆a los datos a la Google Sheet mediante la API de Google Apps Script.
116
+ # :param data: Lista de registros con toda la informaci贸n.
117
+ # """
118
+ # data_dicts = [record.model_dump() for record in data]
119
+ # response = requests.post(GOOGLE_SHEET_API_URL, json=data_dicts)
120
+
121
+ # if response.status_code == 200:
122
+ # print("Datos enviados con 茅xito a la Google Sheet.")
123
+ # else:
124
+ # print(f"Error al enviar datos: {response.status_code}, {response.text}")
125
+
126
+
127
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
128
+
129
+
130
+ def insert_transaccion(fecha, tipo, monto, id_subcategoria, descripcion, id_usuario):
131
+ db = SessionLocal()
132
+
133
+ try:
134
+ # Crear la transacci贸n
135
+ transaccion = Transaccion(
136
+ fecha=fecha,
137
+ tipo=tipo,
138
+ monto=monto,
139
+ descripcion=descripcion,
140
+ id_subcategoria=id_subcategoria,
141
+ id_usuario=id_usuario,
142
+ )
143
+ db.add(transaccion)
144
+ db.commit()
145
+
146
+ transaccion_db = (
147
+ db.query(Transaccion)
148
+ .filter(Transaccion.id_transaccion == transaccion.id_transaccion)
149
+ .first()
150
+ )
151
+
152
+ if transaccion_db:
153
+ subcategoria = transaccion_db.subcategoria.nombre_subcategoria
154
+ categoria = transaccion_db.subcategoria.categoria.nombre_categoria
155
+ usuario = transaccion_db.usuario.nombre
156
+ transaccion_pydantic = TransaccionPydantic(
157
+ fecha=fecha, # Usar fecha directamente
158
+ tipo=tipo,
159
+ monto=monto,
160
+ descripcion=descripcion,
161
+ subcategoria=subcategoria,
162
+ categoria=categoria,
163
+ usuario=usuario,
164
+ )
165
+
166
+ print(transaccion_pydantic.model_dump())
167
+ send_to_google_sheet([transaccion_pydantic])
168
+ finally:
169
+ db.close()
170
+
171
+
172
+ # Funci贸n para obtener los usuarios
173
+ def get_usuarios():
174
+ db = SessionLocal()
175
+ try:
176
+ return db.query(Usuario).all()
177
+ finally:
178
+ db.close()
179
+
180
+
181
+ # Funci贸n para obtener las categor铆as
182
+ def get_categorias():
183
+ db = SessionLocal()
184
+ try:
185
+ return db.query(Categoria).all()
186
+ finally:
187
+ db.close()
188
+
189
+
190
+ # Funci贸n para obtener las categor铆as por tipo
191
+ def get_categorias_por_tipo(tipo):
192
+ db = SessionLocal()
193
+ try:
194
+ return db.query(Categoria).filter(Categoria.tipo == tipo).all()
195
+ finally:
196
+ db.close()
197
+
198
+
199
+ # Funci贸n para obtener las subcategor铆as por categor铆a
200
+ def get_subcategorias(id_categoria=None):
201
+ db = SessionLocal()
202
+ try:
203
+ if id_categoria:
204
+ return (
205
+ db.query(Subcategoria)
206
+ .filter(Subcategoria.id_categoria == id_categoria)
207
+ .all()
208
+ )
209
+ return db.query(Subcategoria).all()
210
+ finally:
211
+ db.close()
212
+
213
+
214
+ # Funci贸n para obtener las transacciones completas
215
+ def get_full_transacciones():
216
+ db = SessionLocal()
217
+ try:
218
+ result = (
219
+ db.query(Transaccion).join(Subcategoria).join(Categoria).join(Usuario).all()
220
+ )
221
+ return [
222
+ {
223
+ "id_transaccion": t.id_transaccion,
224
+ "fecha": t.fecha,
225
+ "tipo": t.tipo,
226
+ "monto": t.monto,
227
+ "descripcion": t.descripcion,
228
+ "nombre_subcategoria": t.subcategoria.nombre_subcategoria,
229
+ "nombre_categoria": t.subcategoria.categoria.nombre_categoria,
230
+ "usuario": t.usuario.nombre,
231
+ }
232
+ for t in result
233
+ ]
234
+ finally:
235
+ db.close()
236
+
237
+
238
+ def get_tipos():
239
+ return ["Ingreso", "Gasto"]
240
+
241
+
242
+ # Definici贸n de modelos Pydantic
243
+
244
+
245
+ class TransaccionPydantic(BaseModel):
246
+ fecha: datetime
247
+ tipo: str
248
+ monto: float
249
+ descripcion: str
250
+ subcategoria: str
251
+ categoria: str
252
+ usuario: str
253
+
254
+
255
+ class CategoriaPydantic(BaseModel):
256
+ id_categoria: int
257
+ nombre_categoria: str
258
+ tipo: str
259
+
260
+
261
+ class SubcategoriaPydantic(BaseModel):
262
+ id_subcategoria: int
263
+ nombre_subcategoria: str
264
+
265
+
266
+ class UsuarioPydantic(BaseModel):
267
+ id_usuario: int
268
+ nombre: str
269
+
270
+
271
+ class TransaccionIngreso(BaseModel):
272
+ fecha: datetime
273
+ monto: float
274
+ tipo: str
275
+ id_subcategoria: int
276
+ descripcion: str
277
+ id_usuario: int
278
+
279
+
280
+ # rutas
281
+ @app.route("/usuarios")
282
+ def usuarios():
283
+ usuarios = get_usuarios()
284
+ return {"usuarios": usuarios}
285
+
286
+
287
+ @app.route("/categorias")
288
+ def categorias():
289
+ categorias = get_categorias()
290
+ return {"categorias": categorias}
291
+
292
+
293
+ # Ruta para obtener categor铆as por tipo a trav茅s de AJAX
294
+ @app.route("/categorias/<tipo>", methods=["GET"])
295
+ def categorias_por_tipo(tipo):
296
+ categorias = get_categorias_por_tipo(tipo) # Obtener las categor铆as por el tipo
297
+ return jsonify(
298
+ [
299
+ {
300
+ "id_categoria": categoria.id_categoria,
301
+ "nombre_categoria": categoria.nombre_categoria,
302
+ "tipo": categoria.tipo,
303
+ }
304
+ for categoria in categorias
305
+ ]
306
+ )
307
+
308
+
309
+ @app.route("/subcategorias")
310
+ def subcategorias():
311
+ subcategorias = get_subcategorias()
312
+ return {"subcategorias": subcategorias}
313
+
314
+
315
+ # Ruta para obtener subcategor铆as por categor铆a a trav茅s de AJAX
316
+ @app.route("/subcategorias/<id_categoria>", methods=["GET"])
317
+ def subcategorias_por_categoria(id_categoria):
318
+ subcategorias = get_subcategorias(id_categoria)
319
+ return jsonify(
320
+ [
321
+ {
322
+ "id_subcategoria": subcategoria.id_subcategoria,
323
+ "nombre_subcategoria": subcategoria.nombre_subcategoria,
324
+ }
325
+ for subcategoria in subcategorias
326
+ ]
327
+ )
328
+
329
+
330
+ # Rutas del API
331
+
332
+
333
+ @app.get("/")
334
+ def home():
335
+ usuarios = get_usuarios()
336
+ categorias = get_categorias()
337
+ subcategorias = get_subcategorias()
338
+ tipos = get_tipos()
339
+ return render_template(
340
+ "index.html",
341
+ usuarios=usuarios,
342
+ categorias=categorias,
343
+ subcategorias=subcategorias,
344
+ tipos=tipos,
345
+ )
346
+
347
+
348
+ @app.post("/guardar-transaccion")
349
+ def guardar_transaccion():
350
+ try:
351
+ # Obtener datos del formulario
352
+ id_usuario = request.form.get("usuario")
353
+ fecha = datetime.now() # Asigna la fecha actual
354
+ tipo = request.form.get("tipo")
355
+ monto = float(request.form.get("monto", 0)) # Convertir a float
356
+ id_subcategoria = request.form.get("subcategoria")
357
+ descripcion = request.form.get("descripcion")
358
+
359
+ # Validar que todos los campos est茅n presentes
360
+ if not all([id_usuario, tipo, monto, id_subcategoria]):
361
+ flash("Por favor complete todos los campos", "error")
362
+ return redirect(url_for("home")) # Redirigir al formulario principal
363
+
364
+ # Insertar la transacci贸n
365
+ insert_transaccion(
366
+ id_usuario=int(id_usuario),
367
+ fecha=fecha,
368
+ tipo=tipo,
369
+ monto=monto,
370
+ id_subcategoria=int(id_subcategoria),
371
+ descripcion=descripcion,
372
+ )
373
+
374
+ flash("Transacci贸n guardada exitosamente", "success")
375
+ return redirect(url_for("home")) # Redirigir al formulario principal
376
+ except Exception as e:
377
+ flash(f"Error al guardar la transacci贸n: {str(e)}", "error")
378
+ return redirect(url_for("home")) # Redirigir al formulario principal
379
+
380
+
381
+ if __name__ == "__main__":
382
+ app.run(host="0.0.0.0", port=8000, debug=False)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask
2
+ requests
3
+ python-dotenv
4
+ Flask-SQLAlchemy
5
+ pytz
6
+ pydantic
7
+ flask-cors
templates/index.html ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Gesti贸n de Ingresos y Gastos</title>
8
+ <script src="https://cdn.tailwindcss.com"></script>
9
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
10
+ <style>
11
+ /* Spinner de carga */
12
+ .spinner {
13
+ border: 4px solid #f3f3f3;
14
+ /* Gris claro */
15
+ border-top: 4px solid #3498db;
16
+ /* Azul */
17
+ border-radius: 50%;
18
+ width: 25px;
19
+ height: 25px;
20
+ animation: spin 2s linear infinite;
21
+ margin: auto;
22
+ }
23
+
24
+ @keyframes spin {
25
+ 0% {
26
+ transform: rotate(0deg);
27
+ }
28
+
29
+ 100% {
30
+ transform: rotate(360deg);
31
+ }
32
+ }
33
+
34
+ /* Spinner de carga */
35
+ #loading {
36
+ text-align: center;
37
+ display: none;
38
+ margin-top: 20px;
39
+ /* Ajuste de la distancia */
40
+ position: relative;
41
+ min-height: 60px;
42
+ /* Altura m铆nima para que el spinner tenga espacio */
43
+ }
44
+
45
+
46
+
47
+ /* Mensajes de flash */
48
+ #mensaje {
49
+ margin-top: 20px;
50
+ }
51
+ </style>
52
+ </head>
53
+
54
+ <body class="bg-indigo-900 text-white">
55
+ <div class="flex items-center justify-center min-h-screen">
56
+ <div class="bg-indigo-800 p-8 rounded-lg shadow-lg w-full max-w-md">
57
+ <h1 class="text-2xl font-bold text-center mb-6">Formulario de Transacci贸n</h1>
58
+
59
+ <!-- Contenedor de mensajes flash (mostrar mensajes aqu铆) -->
60
+ <div id="mensaje" aria-live="polite" class="mb-4"></div>
61
+
62
+ <form id="transaccion-form" action="/guardar-transaccion" method="POST" class="space-y-4"
63
+ onsubmit="mostrarSpinner(event)">
64
+ <div>
65
+ <label for="usuario" class="block text-sm font-medium mb-1">Usuario</label>
66
+ <select id="usuario" name="usuario"
67
+ class="w-full p-2 rounded-lg bg-indigo-700 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500">
68
+ {% for usuario in usuarios %}
69
+ <option value="{{ usuario.id_usuario }}">{{ usuario.nombre }}</option>
70
+ {% endfor %}
71
+ </select>
72
+ </div>
73
+ <div>
74
+ <label for="tipo" class="block text-sm font-medium mb-1">Tipo</label>
75
+ <select id="tipo" name="tipo"
76
+ class="w-full p-2 rounded-lg bg-indigo-700 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500">
77
+ <option value="">Seleccione un tipo</option>
78
+ <option value="Ingreso">Ingreso</option>
79
+ <option value="Gasto">Gasto</option>
80
+ </select>
81
+ </div>
82
+ <div>
83
+ <label for="categoria" class="block text-sm font-medium mb-1">Categor铆a</label>
84
+ <select id="categoria" name="categoria"
85
+ class="w-full p-2 rounded-lg bg-indigo-700 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500">
86
+ <option value="">Seleccione un tipo primero</option>
87
+ </select>
88
+ </div>
89
+ <div>
90
+ <label for="subcategoria" class="block text-sm font-medium mb-1">Subcategor铆a</label>
91
+ <select id="subcategoria" name="subcategoria"
92
+ class="w-full p-2 rounded-lg bg-indigo-700 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500">
93
+ <option value="">Seleccione una categor铆a primero</option>
94
+ </select>
95
+ </div>
96
+ <div>
97
+ <label for="monto" class="block text-sm font-medium mb-1">Monto</label>
98
+ <div class="flex items-center bg-indigo-700 rounded-lg">
99
+ <span class="px-3 text-white font-medium">S/.</span>
100
+ <input type="number" id="monto" name="monto" step="0.1" min="0" value="0.00"
101
+ class="w-full p-2 rounded-r-lg bg-indigo-700 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500">
102
+ </div>
103
+ </div>
104
+
105
+ <div>
106
+ <label for="descripcion" class="block text-sm font-medium mb-1">Descripci贸n</label>
107
+ <textarea id="descripcion" name="descripcion" rows="3"
108
+ class="w-full p-2 rounded-lg bg-indigo-700 text-white focus:outline-none focus:ring-2 focus:ring-indigo-500"></textarea>
109
+ </div>
110
+ <div class="text-center">
111
+ <button type="submit"
112
+ class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded-lg transition duration-300">
113
+ Guardar Transacci贸n
114
+ </button>
115
+ </div>
116
+ </form>
117
+
118
+ <!-- Spinner de carga -->
119
+ <div id="loading">
120
+ <div class="spinner"></div>
121
+ <p>Procesando...</p>
122
+ </div>
123
+ </div>
124
+ </div>
125
+
126
+ <script>
127
+ $(document).ready(function () {
128
+ $('#tipo').change(function () {
129
+ const tipo = $(this).val();
130
+ $.get('/categorias/' + tipo, function (data) {
131
+ $('#categoria').empty();
132
+ if (data.length > 0) {
133
+ data.forEach(function (categoria) {
134
+ $('#categoria').append(
135
+ $('<option>', { value: categoria.id_categoria, text: categoria.nombre_categoria })
136
+ );
137
+ });
138
+ $('#categoria').val(data[0].id_categoria).change();
139
+ } else {
140
+ $('#categoria').append('<option value="">No hay categor铆as disponibles</option>');
141
+ $('#subcategoria').empty().append('<option value="">Seleccione una categor铆a primero</option>');
142
+ }
143
+ });
144
+ });
145
+
146
+ $('#categoria').change(function () {
147
+ const categoriaId = $(this).val();
148
+ $.get('/subcategorias/' + categoriaId, function (data) {
149
+ $('#subcategoria').empty();
150
+ if (data.length > 0) {
151
+ data.forEach(function (subcategoria) {
152
+ $('#subcategoria').append(
153
+ $('<option>', { value: subcategoria.id_subcategoria, text: subcategoria.nombre_subcategoria })
154
+ );
155
+ });
156
+ } else {
157
+ $('#subcategoria').append('<option value="">No hay subcategor铆as disponibles</option>');
158
+ }
159
+ });
160
+ });
161
+
162
+ $('#transaccion-form').on('submit', function (e) {
163
+ e.preventDefault();
164
+ const formData = $(this).serialize();
165
+ $.post('/guardar-transaccion', formData)
166
+ .done(function () {
167
+ $('#mensaje').html('<div class="bg-green-500 p-3 rounded-lg">隆Transacci贸n registrada exitosamente!</div>');
168
+ $('#transaccion-form')[0].reset(); // Reinicia el formulario
169
+ })
170
+ .fail(function () {
171
+ $('#mensaje').html('<div class="bg-red-500 p-3 rounded-lg">Error al registrar transacci贸n</div>');
172
+ })
173
+ .always(function () {
174
+ $('#loading').hide(); // Ocultar el spinner despu茅s de la respuesta
175
+ });
176
+ });
177
+ });
178
+
179
+ function mostrarSpinner(event) {
180
+ $('#loading').show(); // Mostrar el spinner
181
+ }
182
+ </script>
183
+ </body>
184
+
185
+ </html>