Noyer145 commited on
Commit
19beacf
·
verified ·
1 Parent(s): 9f3dce5

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +669 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Autotracker
3
- emoji: 🏢
4
- colorFrom: yellow
5
- colorTo: blue
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: autotracker
3
+ emoji: 🐳
4
+ colorFrom: green
5
+ colorTo: purple
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,669 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Car Expense Manager</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ .chart-container {
11
+ height: 300px;
12
+ position: relative;
13
+ }
14
+ .fade-in {
15
+ animation: fadeIn 0.5s ease-in-out;
16
+ }
17
+ @keyframes fadeIn {
18
+ from { opacity: 0; transform: translateY(10px); }
19
+ to { opacity: 1; transform: translateY(0); }
20
+ }
21
+ .custom-scrollbar::-webkit-scrollbar {
22
+ width: 6px;
23
+ height: 6px;
24
+ }
25
+ .custom-scrollbar::-webkit-scrollbar-track {
26
+ background: #f1f1f1;
27
+ border-radius: 10px;
28
+ }
29
+ .custom-scrollbar::-webkit-scrollbar-thumb {
30
+ background: #888;
31
+ border-radius: 10px;
32
+ }
33
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
34
+ background: #555;
35
+ }
36
+ </style>
37
+ </head>
38
+ <body class="bg-gray-100 min-h-screen">
39
+ <div class="container mx-auto px-4 py-8">
40
+ <header class="mb-10">
41
+ <h1 class="text-4xl font-bold text-center text-indigo-800 mb-2">Car Expense Manager</h1>
42
+ <p class="text-center text-gray-600">Organiza y controla los gastos de tus vehículos</p>
43
+ </header>
44
+
45
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
46
+ <!-- Panel izquierdo - Formularios -->
47
+ <div class="lg:col-span-1 space-y-6">
48
+ <!-- Formulario para agregar auto -->
49
+ <div class="bg-white rounded-xl shadow-md p-6 fade-in">
50
+ <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
51
+ <i class="fas fa-car mr-2 text-indigo-600"></i> Agregar Vehículo
52
+ </h2>
53
+ <form id="addCarForm" class="space-y-4">
54
+ <div>
55
+ <label for="carBrand" class="block text-sm font-medium text-gray-700">Marca</label>
56
+ <input type="text" id="carBrand" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border">
57
+ </div>
58
+ <div>
59
+ <label for="carModel" class="block text-sm font-medium text-gray-700">Modelo</label>
60
+ <input type="text" id="carModel" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border">
61
+ </div>
62
+ <div>
63
+ <label for="carYear" class="block text-sm font-medium text-gray-700">Año</label>
64
+ <input type="number" id="carYear" min="1900" max="2099" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border">
65
+ </div>
66
+ <div>
67
+ <label for="carPlate" class="block text-sm font-medium text-gray-700">Placa</label>
68
+ <input type="text" id="carPlate" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border uppercase">
69
+ </div>
70
+ <button type="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 transition duration-200 flex items-center justify-center">
71
+ <i class="fas fa-plus-circle mr-2"></i> Agregar Vehículo
72
+ </button>
73
+ </form>
74
+ </div>
75
+
76
+ <!-- Formulario para agregar gasto -->
77
+ <div class="bg-white rounded-xl shadow-md p-6 fade-in">
78
+ <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center">
79
+ <i class="fas fa-file-invoice-dollar mr-2 text-indigo-600"></i> Registrar Gasto
80
+ </h2>
81
+ <form id="addExpenseForm" class="space-y-4">
82
+ <div>
83
+ <label for="expenseCar" class="block text-sm font-medium text-gray-700">Vehículo</label>
84
+ <select id="expenseCar" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border">
85
+ <option value="" disabled selected>Selecciona un vehículo</option>
86
+ </select>
87
+ </div>
88
+ <div>
89
+ <label for="expenseType" class="block text-sm font-medium text-gray-700">Tipo de gasto</label>
90
+ <select id="expenseType" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border">
91
+ <option value="" disabled selected>Selecciona un tipo</option>
92
+ <option value="Mantenimiento">Mantenimiento</option>
93
+ <option value="Reparación">Reparación</option>
94
+ <option value="Documentos">Documentos</option>
95
+ <option value="Seguro">Seguro</option>
96
+ <option value="Combustible">Combustible</option>
97
+ <option value="Otro">Otro</option>
98
+ </select>
99
+ </div>
100
+ <div>
101
+ <label for="expenseAmount" class="block text-sm font-medium text-gray-700">Monto ($)</label>
102
+ <input type="number" id="expenseAmount" min="0" step="0.01" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border">
103
+ </div>
104
+ <div>
105
+ <label for="expenseDate" class="block text-sm font-medium text-gray-700">Fecha</label>
106
+ <input type="date" id="expenseDate" required class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border">
107
+ </div>
108
+ <div>
109
+ <label for="expenseDescription" class="block text-sm font-medium text-gray-700">Descripción</label>
110
+ <textarea id="expenseDescription" rows="2" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border"></textarea>
111
+ </div>
112
+ <button type="submit" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 transition duration-200 flex items-center justify-center">
113
+ <i class="fas fa-save mr-2"></i> Guardar Gasto
114
+ </button>
115
+ </form>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Panel derecho - Visualización de datos -->
120
+ <div class="lg:col-span-2 space-y-6">
121
+ <!-- Filtros y resumen -->
122
+ <div class="bg-white rounded-xl shadow-md p-6 fade-in">
123
+ <div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6">
124
+ <h2 class="text-xl font-semibold text-gray-800 flex items-center">
125
+ <i class="fas fa-chart-pie mr-2 text-indigo-600"></i> Resumen de Gastos
126
+ </h2>
127
+ <div class="flex flex-col sm:flex-row gap-3 mt-4 md:mt-0">
128
+ <select id="filterCar" class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border text-sm">
129
+ <option value="all">Todos los vehículos</option>
130
+ </select>
131
+ <select id="filterYear" class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border text-sm">
132
+ <option value="all">Todos los años</option>
133
+ </select>
134
+ <select id="filterType" class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border text-sm">
135
+ <option value="all">Todos los tipos</option>
136
+ <option value="Mantenimiento">Mantenimiento</option>
137
+ <option value="Reparación">Reparación</option>
138
+ <option value="Documentos">Documentos</option>
139
+ <option value="Seguro">Seguro</option>
140
+ <option value="Combustible">Combustible</option>
141
+ <option value="Otro">Otro</option>
142
+ </select>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Tarjetas de resumen -->
147
+ <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6">
148
+ <div class="bg-indigo-50 rounded-lg p-4 border border-indigo-100">
149
+ <p class="text-sm text-indigo-800 font-medium">Total Gastado</p>
150
+ <p id="totalSpent" class="text-2xl font-bold text-indigo-600">$0.00</p>
151
+ </div>
152
+ <div class="bg-green-50 rounded-lg p-4 border border-green-100">
153
+ <p class="text-sm text-green-800 font-medium">Vehículos Registrados</p>
154
+ <p id="totalCars" class="text-2xl font-bold text-green-600">0</p>
155
+ </div>
156
+ <div class="bg-purple-50 rounded-lg p-4 border border-purple-100">
157
+ <p class="text-sm text-purple-800 font-medium">Gastos Registrados</p>
158
+ <p id="totalExpenses" class="text-2xl font-bold text-purple-600">0</p>
159
+ </div>
160
+ </div>
161
+
162
+ <!-- Gráfico (simulado) -->
163
+ <div class="bg-gray-50 rounded-lg p-4 border border-gray-200 mb-6">
164
+ <div class="flex justify-between items-center mb-3">
165
+ <h3 class="font-medium text-gray-700">Distribución de Gastos</h3>
166
+ <div class="flex items-center text-sm">
167
+ <span class="w-3 h-3 rounded-full bg-indigo-500 mr-1"></span>
168
+ <span id="chartLegend" class="text-gray-600">Selecciona filtros</span>
169
+ </div>
170
+ </div>
171
+ <div class="chart-container">
172
+ <div id="expenseChart" class="h-full flex items-end justify-around">
173
+ <!-- Las barras del gráfico se generarán dinámicamente -->
174
+ <div class="text-center text-xs text-gray-500">
175
+ No hay datos para mostrar
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ <!-- Lista de gastos -->
183
+ <div class="bg-white rounded-xl shadow-md p-6 fade-in">
184
+ <div class="flex justify-between items-center mb-4">
185
+ <h2 class="text-xl font-semibold text-gray-800 flex items-center">
186
+ <i class="fas fa-receipt mr-2 text-indigo-600"></i> Historial de Gastos
187
+ </h2>
188
+ <button id="exportBtn" class="text-sm bg-gray-100 hover:bg-gray-200 text-gray-800 py-1 px-3 rounded-md flex items-center transition duration-200">
189
+ <i class="fas fa-download mr-1"></i> Exportar
190
+ </button>
191
+ </div>
192
+
193
+ <div class="overflow-x-auto custom-scrollbar">
194
+ <table class="min-w-full divide-y divide-gray-200">
195
+ <thead class="bg-gray-50">
196
+ <tr>
197
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fecha</th>
198
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Vehículo</th>
199
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipo</th>
200
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Descripción</th>
201
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Monto</th>
202
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Acciones</th>
203
+ </tr>
204
+ </thead>
205
+ <tbody id="expensesTableBody" class="bg-white divide-y divide-gray-200">
206
+ <tr>
207
+ <td colspan="6" class="px-6 py-4 text-center text-sm text-gray-500">No hay gastos registrados</td>
208
+ </tr>
209
+ </tbody>
210
+ </table>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ <!-- Modal para confirmar eliminación -->
218
+ <div id="confirmModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
219
+ <div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
220
+ <div class="flex items-center mb-4">
221
+ <div class="bg-red-100 p-3 rounded-full mr-3">
222
+ <i class="fas fa-exclamation-triangle text-red-600"></i>
223
+ </div>
224
+ <h3 class="text-lg font-medium text-gray-900">Confirmar eliminación</h3>
225
+ </div>
226
+ <p class="text-gray-600 mb-6">¿Estás seguro que deseas eliminar este gasto? Esta acción no se puede deshacer.</p>
227
+ <div class="flex justify-end space-x-3">
228
+ <button id="cancelDelete" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">Cancelar</button>
229
+ <button id="confirmDelete" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">Eliminar</button>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <script>
235
+ // Datos iniciales (simulando una base de datos)
236
+ let cars = JSON.parse(localStorage.getItem('cars')) || [];
237
+ let expenses = JSON.parse(localStorage.getItem('expenses')) || [];
238
+ let expenseToDelete = null;
239
+
240
+ // Elementos del DOM
241
+ const addCarForm = document.getElementById('addCarForm');
242
+ const addExpenseForm = document.getElementById('addExpenseForm');
243
+ const expenseCarSelect = document.getElementById('expenseCar');
244
+ const filterCarSelect = document.getElementById('filterCar');
245
+ const filterYearSelect = document.getElementById('filterYear');
246
+ const filterTypeSelect = document.getElementById('filterType');
247
+ const expensesTableBody = document.getElementById('expensesTableBody');
248
+ const totalSpentElement = document.getElementById('totalSpent');
249
+ const totalCarsElement = document.getElementById('totalCars');
250
+ const totalExpensesElement = document.getElementById('totalExpenses');
251
+ const expenseChart = document.getElementById('expenseChart');
252
+ const chartLegend = document.getElementById('chartLegend');
253
+ const confirmModal = document.getElementById('confirmModal');
254
+ const cancelDeleteBtn = document.getElementById('cancelDelete');
255
+ const confirmDeleteBtn = document.getElementById('confirmDelete');
256
+ const exportBtn = document.getElementById('exportBtn');
257
+
258
+ // Inicializar la aplicación
259
+ document.addEventListener('DOMContentLoaded', function() {
260
+ updateCarSelects();
261
+ updateYearFilter();
262
+ renderExpenses();
263
+ updateSummary();
264
+ renderChart();
265
+
266
+ // Configurar fecha actual como predeterminada
267
+ const today = new Date().toISOString().split('T')[0];
268
+ document.getElementById('expenseDate').value = today;
269
+ });
270
+
271
+ // Event listeners
272
+ addCarForm.addEventListener('submit', function(e) {
273
+ e.preventDefault();
274
+ addCar();
275
+ });
276
+
277
+ addExpenseForm.addEventListener('submit', function(e) {
278
+ e.preventDefault();
279
+ addExpense();
280
+ });
281
+
282
+ [filterCarSelect, filterYearSelect, filterTypeSelect].forEach(filter => {
283
+ filter.addEventListener('change', function() {
284
+ renderExpenses();
285
+ updateSummary();
286
+ renderChart();
287
+ });
288
+ });
289
+
290
+ cancelDeleteBtn.addEventListener('click', function() {
291
+ confirmModal.classList.add('hidden');
292
+ });
293
+
294
+ confirmDeleteBtn.addEventListener('click', function() {
295
+ if (expenseToDelete !== null) {
296
+ deleteExpense(expenseToDelete);
297
+ confirmModal.classList.add('hidden');
298
+ expenseToDelete = null;
299
+ }
300
+ });
301
+
302
+ exportBtn.addEventListener('click', exportData);
303
+
304
+ // Funciones principales
305
+ function addCar() {
306
+ const brand = document.getElementById('carBrand').value.trim();
307
+ const model = document.getElementById('carModel').value.trim();
308
+ const year = document.getElementById('carYear').value;
309
+ const plate = document.getElementById('carPlate').value.trim().toUpperCase();
310
+
311
+ const newCar = {
312
+ id: Date.now(),
313
+ brand,
314
+ model,
315
+ year: parseInt(year),
316
+ plate
317
+ };
318
+
319
+ cars.push(newCar);
320
+ saveData();
321
+ updateCarSelects();
322
+ document.getElementById('addCarForm').reset();
323
+
324
+ // Mostrar notificación
325
+ showNotification('Vehículo agregado correctamente', 'success');
326
+ }
327
+
328
+ function addExpense() {
329
+ const carId = parseInt(document.getElementById('expenseCar').value);
330
+ const type = document.getElementById('expenseType').value;
331
+ const amount = parseFloat(document.getElementById('expenseAmount').value);
332
+ const date = document.getElementById('expenseDate').value;
333
+ const description = document.getElementById('expenseDescription').value.trim();
334
+
335
+ const car = cars.find(c => c.id === carId);
336
+ if (!car) return;
337
+
338
+ const newExpense = {
339
+ id: Date.now(),
340
+ carId,
341
+ carBrand: car.brand,
342
+ carModel: car.model,
343
+ carPlate: car.plate,
344
+ type,
345
+ amount,
346
+ date,
347
+ description,
348
+ createdAt: new Date().toISOString()
349
+ };
350
+
351
+ expenses.push(newExpense);
352
+ saveData();
353
+ renderExpenses();
354
+ updateSummary();
355
+ renderChart();
356
+ document.getElementById('addExpenseForm').reset();
357
+
358
+ // Mostrar notificación
359
+ showNotification('Gasto registrado correctamente', 'success');
360
+ }
361
+
362
+ function deleteExpense(expenseId) {
363
+ expenses = expenses.filter(expense => expense.id !== expenseId);
364
+ saveData();
365
+ renderExpenses();
366
+ updateSummary();
367
+ renderChart();
368
+
369
+ // Mostrar notificación
370
+ showNotification('Gasto eliminado correctamente', 'success');
371
+ }
372
+
373
+ function updateCarSelects() {
374
+ // Limpiar selects
375
+ expenseCarSelect.innerHTML = '<option value="" disabled selected>Selecciona un vehículo</option>';
376
+ filterCarSelect.innerHTML = '<option value="all">Todos los vehículos</option>';
377
+
378
+ // Llenar selects con los vehículos disponibles
379
+ cars.forEach(car => {
380
+ const option1 = document.createElement('option');
381
+ option1.value = car.id;
382
+ option1.textContent = `${car.brand} ${car.model} (${car.plate})`;
383
+ expenseCarSelect.appendChild(option1);
384
+
385
+ const option2 = document.createElement('option');
386
+ option2.value = car.id;
387
+ option2.textContent = `${car.brand} ${car.model} (${car.plate})`;
388
+ filterCarSelect.appendChild(option2);
389
+ });
390
+
391
+ // Actualizar contador de vehículos
392
+ totalCarsElement.textContent = cars.length;
393
+ }
394
+
395
+ function updateYearFilter() {
396
+ // Obtener años únicos de los gastos
397
+ const years = [...new Set(expenses.map(expense => expense.date.split('-')[0]))];
398
+
399
+ // Ordenar años de más reciente a más antiguo
400
+ years.sort((a, b) => b - a);
401
+
402
+ // Limpiar y llenar el select
403
+ filterYearSelect.innerHTML = '<option value="all">Todos los años</option>';
404
+ years.forEach(year => {
405
+ const option = document.createElement('option');
406
+ option.value = year;
407
+ option.textContent = year;
408
+ filterYearSelect.appendChild(option);
409
+ });
410
+ }
411
+
412
+ function renderExpenses() {
413
+ const filteredExpenses = filterExpenses();
414
+
415
+ // Limpiar tabla
416
+ expensesTableBody.innerHTML = '';
417
+
418
+ if (filteredExpenses.length === 0) {
419
+ expensesTableBody.innerHTML = `
420
+ <tr>
421
+ <td colspan="6" class="px-6 py-4 text-center text-sm text-gray-500">No hay gastos que coincidan con los filtros</td>
422
+ </tr>
423
+ `;
424
+ return;
425
+ }
426
+
427
+ // Ordenar gastos por fecha (más reciente primero)
428
+ filteredExpenses.sort((a, b) => new Date(b.date) - new Date(a.date));
429
+
430
+ // Llenar tabla con los gastos filtrados
431
+ filteredExpenses.forEach(expense => {
432
+ const row = document.createElement('tr');
433
+ row.className = 'hover:bg-gray-50';
434
+
435
+ // Formatear fecha
436
+ const date = new Date(expense.date);
437
+ const formattedDate = date.toLocaleDateString('es-ES', {
438
+ day: '2-digit',
439
+ month: '2-digit',
440
+ year: 'numeric'
441
+ });
442
+
443
+ // Determinar color según el tipo de gasto
444
+ let typeColor = 'gray';
445
+ switch(expense.type) {
446
+ case 'Mantenimiento': typeColor = 'blue'; break;
447
+ case 'Reparación': typeColor = 'red'; break;
448
+ case 'Documentos': typeColor = 'green'; break;
449
+ case 'Seguro': typeColor = 'purple'; break;
450
+ case 'Combustible': typeColor = 'orange'; break;
451
+ case 'Otro': typeColor = 'gray'; break;
452
+ }
453
+
454
+ row.innerHTML = `
455
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedDate}</td>
456
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${expense.carBrand} ${expense.carModel}</td>
457
+ <td class="px-6 py-4 whitespace-nowrap">
458
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-${typeColor}-100 text-${typeColor}-800">
459
+ ${expense.type}
460
+ </span>
461
+ </td>
462
+ <td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">${expense.description || 'Sin descripción'}</td>
463
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">$${expense.amount.toFixed(2)}</td>
464
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
465
+ <button class="text-red-600 hover:text-red-900 delete-btn" data-id="${expense.id}">
466
+ <i class="fas fa-trash-alt"></i>
467
+ </button>
468
+ </td>
469
+ `;
470
+
471
+ expensesTableBody.appendChild(row);
472
+ });
473
+
474
+ // Agregar event listeners a los botones de eliminar
475
+ document.querySelectorAll('.delete-btn').forEach(btn => {
476
+ btn.addEventListener('click', function() {
477
+ expenseToDelete = parseInt(this.getAttribute('data-id'));
478
+ confirmModal.classList.remove('hidden');
479
+ });
480
+ });
481
+
482
+ // Actualizar contador de gastos
483
+ totalExpensesElement.textContent = filteredExpenses.length;
484
+ }
485
+
486
+ function filterExpenses() {
487
+ const carId = filterCarSelect.value === 'all' ? null : parseInt(filterCarSelect.value);
488
+ const year = filterYearSelect.value === 'all' ? null : filterYearSelect.value;
489
+ const type = filterTypeSelect.value === 'all' ? null : filterTypeSelect.value;
490
+
491
+ return expenses.filter(expense => {
492
+ // Filtrar por vehículo
493
+ if (carId !== null && expense.carId !== carId) return false;
494
+
495
+ // Filtrar por año
496
+ if (year !== null && !expense.date.startsWith(year)) return false;
497
+
498
+ // Filtrar por tipo
499
+ if (type !== null && expense.type !== type) return false;
500
+
501
+ return true;
502
+ });
503
+ }
504
+
505
+ function updateSummary() {
506
+ const filteredExpenses = filterExpenses();
507
+
508
+ // Calcular total gastado
509
+ const total = filteredExpenses.reduce((sum, expense) => sum + expense.amount, 0);
510
+ totalSpentElement.textContent = `$${total.toFixed(2)}`;
511
+
512
+ // Actualizar leyenda del gráfico
513
+ let legendText = 'Todos los gastos';
514
+ if (filterCarSelect.value !== 'all') {
515
+ const selectedCar = cars.find(c => c.id === parseInt(filterCarSelect.value));
516
+ legendText = `${selectedCar.brand} ${selectedCar.model}`;
517
+ }
518
+ if (filterYearSelect.value !== 'all') {
519
+ legendText += ` - Año ${filterYearSelect.value}`;
520
+ }
521
+ if (filterTypeSelect.value !== 'all') {
522
+ legendText += ` - ${filterTypeSelect.value}`;
523
+ }
524
+ chartLegend.textContent = legendText;
525
+ }
526
+
527
+ function renderChart() {
528
+ const filteredExpenses = filterExpenses();
529
+
530
+ // Limpiar gráfico
531
+ expenseChart.innerHTML = '';
532
+
533
+ if (filteredExpenses.length === 0) {
534
+ expenseChart.innerHTML = '<div class="text-center text-xs text-gray-500">No hay datos para mostrar</div>';
535
+ return;
536
+ }
537
+
538
+ // Agrupar gastos por tipo si no hay filtro de tipo, o por mes si hay filtro de tipo
539
+ let data = {};
540
+ let isGroupedByType = filterTypeSelect.value === 'all';
541
+
542
+ if (isGroupedByType) {
543
+ // Agrupar por tipo de gasto
544
+ filteredExpenses.forEach(expense => {
545
+ if (!data[expense.type]) {
546
+ data[expense.type] = 0;
547
+ }
548
+ data[expense.type] += expense.amount;
549
+ });
550
+ } else {
551
+ // Agrupar por mes
552
+ filteredExpenses.forEach(expense => {
553
+ const month = expense.date.substring(0, 7); // Formato YYYY-MM
554
+ if (!data[month]) {
555
+ data[month] = 0;
556
+ }
557
+ data[month] += expense.amount;
558
+ });
559
+ }
560
+
561
+ // Ordenar los datos
562
+ const sortedData = Object.entries(data).sort((a, b) => {
563
+ if (isGroupedByType) return a[0].localeCompare(b[0]);
564
+ return a[0].localeCompare(b[0]);
565
+ });
566
+
567
+ // Calcular el máximo valor para escalar las barras
568
+ const maxValue = Math.max(...Object.values(data));
569
+
570
+ // Generar las barras del gráfico
571
+ sortedData.forEach(([label, value]) => {
572
+ const barHeight = maxValue > 0 ? (value / maxValue * 100) : 0;
573
+ const barColor = getRandomColor();
574
+
575
+ const barContainer = document.createElement('div');
576
+ barContainer.className = 'flex flex-col items-center w-full';
577
+
578
+ // Formatear el label para meses
579
+ let displayLabel = label;
580
+ if (!isGroupedByType) {
581
+ const [year, month] = label.split('-');
582
+ displayLabel = new Date(year, month-1).toLocaleDateString('es-ES', { month: 'short' });
583
+ }
584
+
585
+ barContainer.innerHTML = `
586
+ <div class="w-8 bg-${barColor}-500 rounded-t-md hover:bg-${barColor}-600 transition duration-200" style="height: ${barHeight}%"></div>
587
+ <div class="text-xs text-gray-500 mt-1 text-center">${displayLabel}</div>
588
+ <div class="text-xs font-medium mt-1">$${value.toFixed(2)}</div>
589
+ `;
590
+
591
+ expenseChart.appendChild(barContainer);
592
+ });
593
+ }
594
+
595
+ function getRandomColor() {
596
+ const colors = ['indigo', 'blue', 'green', 'yellow', 'red', 'purple', 'pink', 'orange'];
597
+ return colors[Math.floor(Math.random() * colors.length)];
598
+ }
599
+
600
+ function saveData() {
601
+ localStorage.setItem('cars', JSON.stringify(cars));
602
+ localStorage.setItem('expenses', JSON.stringify(expenses));
603
+ }
604
+
605
+ function exportData() {
606
+ const filteredExpenses = filterExpenses();
607
+
608
+ if (filteredExpenses.length === 0) {
609
+ showNotification('No hay datos para exportar', 'warning');
610
+ return;
611
+ }
612
+
613
+ // Crear contenido CSV
614
+ let csvContent = "Fecha,Vehículo,Tipo,Descripción,Monto\n";
615
+
616
+ filteredExpenses.forEach(expense => {
617
+ const date = new Date(expense.date).toLocaleDateString('es-ES');
618
+ const carInfo = `${expense.carBrand} ${expense.carModel} (${expense.carPlate})`;
619
+ const description = expense.description ? `"${expense.description.replace(/"/g, '""')}"` : '';
620
+
621
+ csvContent += `${date},${carInfo},${expense.type},${description},${expense.amount.toFixed(2)}\n`;
622
+ });
623
+
624
+ // Crear y descargar archivo
625
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
626
+ const url = URL.createObjectURL(blob);
627
+ const link = document.createElement('a');
628
+ link.setAttribute('href', url);
629
+ link.setAttribute('download', `gastos_vehiculos_${new Date().toISOString().slice(0, 10)}.csv`);
630
+ link.style.visibility = 'hidden';
631
+ document.body.appendChild(link);
632
+ link.click();
633
+ document.body.removeChild(link);
634
+
635
+ // Mostrar notificación
636
+ showNotification('Datos exportados correctamente', 'success');
637
+ }
638
+
639
+ function showNotification(message, type) {
640
+ const notification = document.createElement('div');
641
+ notification.className = `fixed bottom-4 right-4 px-4 py-2 rounded-md shadow-md text-white ${
642
+ type === 'success' ? 'bg-green-500' :
643
+ type === 'error' ? 'bg-red-500' :
644
+ 'bg-yellow-500'
645
+ } flex items-center`;
646
+
647
+ notification.innerHTML = `
648
+ <i class="fas ${
649
+ type === 'success' ? 'fa-check-circle' :
650
+ type === 'error' ? 'fa-exclamation-circle' :
651
+ 'fa-exclamation-triangle'
652
+ } mr-2"></i>
653
+ <span>${message}</span>
654
+ `;
655
+
656
+ document.body.appendChild(notification);
657
+
658
+ // Desvanecer y eliminar la notificación después de 3 segundos
659
+ setTimeout(() => {
660
+ notification.style.opacity = '0';
661
+ notification.style.transition = 'opacity 0.5s ease-out';
662
+ setTimeout(() => {
663
+ notification.remove();
664
+ }, 500);
665
+ }, 3000);
666
+ }
667
+ </script>
668
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Noyer145/autotracker" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
669
+ </html>