| <!DOCTYPE html> |
| <html lang="es"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Car Expense Manager</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .chart-container { |
| height: 300px; |
| position: relative; |
| } |
| .fade-in { |
| animation: fadeIn 0.5s ease-in-out; |
| } |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translateY(10px); } |
| to { opacity: 1; transform: translateY(0); } |
| } |
| .custom-scrollbar::-webkit-scrollbar { |
| width: 6px; |
| height: 6px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-track { |
| background: #f1f1f1; |
| border-radius: 10px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-thumb { |
| background: #888; |
| border-radius: 10px; |
| } |
| .custom-scrollbar::-webkit-scrollbar-thumb:hover { |
| background: #555; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="mb-10"> |
| <h1 class="text-4xl font-bold text-center text-indigo-800 mb-2">Car Expense Manager</h1> |
| <p class="text-center text-gray-600">Organiza y controla los gastos de tus veh铆culos</p> |
| </header> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <div class="lg:col-span-1 space-y-6"> |
| |
| <div class="bg-white rounded-xl shadow-md p-6 fade-in"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
| <i class="fas fa-car mr-2 text-indigo-600"></i> Agregar Veh铆culo |
| </h2> |
| <form id="addCarForm" class="space-y-4"> |
| <div> |
| <label for="carBrand" class="block text-sm font-medium text-gray-700">Marca</label> |
| <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"> |
| </div> |
| <div> |
| <label for="carModel" class="block text-sm font-medium text-gray-700">Modelo</label> |
| <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"> |
| </div> |
| <div> |
| <label for="carYear" class="block text-sm font-medium text-gray-700">A帽o</label> |
| <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"> |
| </div> |
| <div> |
| <label for="carPlate" class="block text-sm font-medium text-gray-700">Placa</label> |
| <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"> |
| </div> |
| <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"> |
| <i class="fas fa-plus-circle mr-2"></i> Agregar Veh铆culo |
| </button> |
| </form> |
| </div> |
|
|
| |
| <div class="bg-white rounded-xl shadow-md p-6 fade-in"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
| <i class="fas fa-file-invoice-dollar mr-2 text-indigo-600"></i> Registrar Gasto |
| </h2> |
| <form id="addExpenseForm" class="space-y-4"> |
| <div> |
| <label for="expenseCar" class="block text-sm font-medium text-gray-700">Veh铆culo</label> |
| <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"> |
| <option value="" disabled selected>Selecciona un veh铆culo</option> |
| </select> |
| </div> |
| <div> |
| <label for="expenseType" class="block text-sm font-medium text-gray-700">Tipo de gasto</label> |
| <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"> |
| <option value="" disabled selected>Selecciona un tipo</option> |
| <option value="Mantenimiento">Mantenimiento</option> |
| <option value="Reparaci贸n">Reparaci贸n</option> |
| <option value="Documentos">Documentos</option> |
| <option value="Seguro">Seguro</option> |
| <option value="Combustible">Combustible</option> |
| <option value="Otro">Otro</option> |
| </select> |
| </div> |
| <div> |
| <label for="expenseAmount" class="block text-sm font-medium text-gray-700">Monto ($)</label> |
| <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"> |
| </div> |
| <div> |
| <label for="expenseDate" class="block text-sm font-medium text-gray-700">Fecha</label> |
| <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"> |
| </div> |
| <div> |
| <label for="expenseDescription" class="block text-sm font-medium text-gray-700">Descripci贸n</label> |
| <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> |
| </div> |
| <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"> |
| <i class="fas fa-save mr-2"></i> Guardar Gasto |
| </button> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div class="lg:col-span-2 space-y-6"> |
| |
| <div class="bg-white rounded-xl shadow-md p-6 fade-in"> |
| <div class="flex flex-col md:flex-row md:items-center md:justify-between mb-6"> |
| <h2 class="text-xl font-semibold text-gray-800 flex items-center"> |
| <i class="fas fa-chart-pie mr-2 text-indigo-600"></i> Resumen de Gastos |
| </h2> |
| <div class="flex flex-col sm:flex-row gap-3 mt-4 md:mt-0"> |
| <select id="filterCar" class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border text-sm"> |
| <option value="all">Todos los veh铆culos</option> |
| </select> |
| <select id="filterYear" class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border text-sm"> |
| <option value="all">Todos los a帽os</option> |
| </select> |
| <select id="filterType" class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 p-2 border text-sm"> |
| <option value="all">Todos los tipos</option> |
| <option value="Mantenimiento">Mantenimiento</option> |
| <option value="Reparaci贸n">Reparaci贸n</option> |
| <option value="Documentos">Documentos</option> |
| <option value="Seguro">Seguro</option> |
| <option value="Combustible">Combustible</option> |
| <option value="Otro">Otro</option> |
| </select> |
| </div> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-6"> |
| <div class="bg-indigo-50 rounded-lg p-4 border border-indigo-100"> |
| <p class="text-sm text-indigo-800 font-medium">Total Gastado</p> |
| <p id="totalSpent" class="text-2xl font-bold text-indigo-600">$0.00</p> |
| </div> |
| <div class="bg-green-50 rounded-lg p-4 border border-green-100"> |
| <p class="text-sm text-green-800 font-medium">Veh铆culos Registrados</p> |
| <p id="totalCars" class="text-2xl font-bold text-green-600">0</p> |
| </div> |
| <div class="bg-purple-50 rounded-lg p-4 border border-purple-100"> |
| <p class="text-sm text-purple-800 font-medium">Gastos Registrados</p> |
| <p id="totalExpenses" class="text-2xl font-bold text-purple-600">0</p> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-gray-50 rounded-lg p-4 border border-gray-200 mb-6"> |
| <div class="flex justify-between items-center mb-3"> |
| <h3 class="font-medium text-gray-700">Distribuci贸n de Gastos</h3> |
| <div class="flex items-center text-sm"> |
| <span class="w-3 h-3 rounded-full bg-indigo-500 mr-1"></span> |
| <span id="chartLegend" class="text-gray-600">Selecciona filtros</span> |
| </div> |
| </div> |
| <div class="chart-container"> |
| <div id="expenseChart" class="h-full flex items-end justify-around"> |
| |
| <div class="text-center text-xs text-gray-500"> |
| No hay datos para mostrar |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-xl shadow-md p-6 fade-in"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-xl font-semibold text-gray-800 flex items-center"> |
| <i class="fas fa-receipt mr-2 text-indigo-600"></i> Historial de Gastos |
| </h2> |
| <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"> |
| <i class="fas fa-download mr-1"></i> Exportar |
| </button> |
| </div> |
| |
| <div class="overflow-x-auto custom-scrollbar"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Fecha</th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Veh铆culo</th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tipo</th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Descripci贸n</th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Monto</th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Acciones</th> |
| </tr> |
| </thead> |
| <tbody id="expensesTableBody" class="bg-white divide-y divide-gray-200"> |
| <tr> |
| <td colspan="6" class="px-6 py-4 text-center text-sm text-gray-500">No hay gastos registrados</td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="confirmModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg p-6 max-w-md w-full mx-4"> |
| <div class="flex items-center mb-4"> |
| <div class="bg-red-100 p-3 rounded-full mr-3"> |
| <i class="fas fa-exclamation-triangle text-red-600"></i> |
| </div> |
| <h3 class="text-lg font-medium text-gray-900">Confirmar eliminaci贸n</h3> |
| </div> |
| <p class="text-gray-600 mb-6">驴Est谩s seguro que deseas eliminar este gasto? Esta acci贸n no se puede deshacer.</p> |
| <div class="flex justify-end space-x-3"> |
| <button id="cancelDelete" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50">Cancelar</button> |
| <button id="confirmDelete" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">Eliminar</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| // Datos iniciales (simulando una base de datos) |
| let cars = JSON.parse(localStorage.getItem('cars')) || []; |
| let expenses = JSON.parse(localStorage.getItem('expenses')) || []; |
| let expenseToDelete = null; |
| |
| // Elementos del DOM |
| const addCarForm = document.getElementById('addCarForm'); |
| const addExpenseForm = document.getElementById('addExpenseForm'); |
| const expenseCarSelect = document.getElementById('expenseCar'); |
| const filterCarSelect = document.getElementById('filterCar'); |
| const filterYearSelect = document.getElementById('filterYear'); |
| const filterTypeSelect = document.getElementById('filterType'); |
| const expensesTableBody = document.getElementById('expensesTableBody'); |
| const totalSpentElement = document.getElementById('totalSpent'); |
| const totalCarsElement = document.getElementById('totalCars'); |
| const totalExpensesElement = document.getElementById('totalExpenses'); |
| const expenseChart = document.getElementById('expenseChart'); |
| const chartLegend = document.getElementById('chartLegend'); |
| const confirmModal = document.getElementById('confirmModal'); |
| const cancelDeleteBtn = document.getElementById('cancelDelete'); |
| const confirmDeleteBtn = document.getElementById('confirmDelete'); |
| const exportBtn = document.getElementById('exportBtn'); |
| |
| // Inicializar la aplicaci贸n |
| document.addEventListener('DOMContentLoaded', function() { |
| updateCarSelects(); |
| updateYearFilter(); |
| renderExpenses(); |
| updateSummary(); |
| renderChart(); |
| |
| // Configurar fecha actual como predeterminada |
| const today = new Date().toISOString().split('T')[0]; |
| document.getElementById('expenseDate').value = today; |
| }); |
| |
| // Event listeners |
| addCarForm.addEventListener('submit', function(e) { |
| e.preventDefault(); |
| addCar(); |
| }); |
| |
| addExpenseForm.addEventListener('submit', function(e) { |
| e.preventDefault(); |
| addExpense(); |
| }); |
| |
| [filterCarSelect, filterYearSelect, filterTypeSelect].forEach(filter => { |
| filter.addEventListener('change', function() { |
| renderExpenses(); |
| updateSummary(); |
| renderChart(); |
| }); |
| }); |
| |
| cancelDeleteBtn.addEventListener('click', function() { |
| confirmModal.classList.add('hidden'); |
| }); |
| |
| confirmDeleteBtn.addEventListener('click', function() { |
| if (expenseToDelete !== null) { |
| deleteExpense(expenseToDelete); |
| confirmModal.classList.add('hidden'); |
| expenseToDelete = null; |
| } |
| }); |
| |
| exportBtn.addEventListener('click', exportData); |
| |
| // Funciones principales |
| function addCar() { |
| const brand = document.getElementById('carBrand').value.trim(); |
| const model = document.getElementById('carModel').value.trim(); |
| const year = document.getElementById('carYear').value; |
| const plate = document.getElementById('carPlate').value.trim().toUpperCase(); |
| |
| const newCar = { |
| id: Date.now(), |
| brand, |
| model, |
| year: parseInt(year), |
| plate |
| }; |
| |
| cars.push(newCar); |
| saveData(); |
| updateCarSelects(); |
| document.getElementById('addCarForm').reset(); |
| |
| // Mostrar notificaci贸n |
| showNotification('Veh铆culo agregado correctamente', 'success'); |
| } |
| |
| function addExpense() { |
| const carId = parseInt(document.getElementById('expenseCar').value); |
| const type = document.getElementById('expenseType').value; |
| const amount = parseFloat(document.getElementById('expenseAmount').value); |
| const date = document.getElementById('expenseDate').value; |
| const description = document.getElementById('expenseDescription').value.trim(); |
| |
| const car = cars.find(c => c.id === carId); |
| if (!car) return; |
| |
| const newExpense = { |
| id: Date.now(), |
| carId, |
| carBrand: car.brand, |
| carModel: car.model, |
| carPlate: car.plate, |
| type, |
| amount, |
| date, |
| description, |
| createdAt: new Date().toISOString() |
| }; |
| |
| expenses.push(newExpense); |
| saveData(); |
| renderExpenses(); |
| updateSummary(); |
| renderChart(); |
| document.getElementById('addExpenseForm').reset(); |
| |
| // Mostrar notificaci贸n |
| showNotification('Gasto registrado correctamente', 'success'); |
| } |
| |
| function deleteExpense(expenseId) { |
| expenses = expenses.filter(expense => expense.id !== expenseId); |
| saveData(); |
| renderExpenses(); |
| updateSummary(); |
| renderChart(); |
| |
| // Mostrar notificaci贸n |
| showNotification('Gasto eliminado correctamente', 'success'); |
| } |
| |
| function updateCarSelects() { |
| // Limpiar selects |
| expenseCarSelect.innerHTML = '<option value="" disabled selected>Selecciona un veh铆culo</option>'; |
| filterCarSelect.innerHTML = '<option value="all">Todos los veh铆culos</option>'; |
| |
| // Llenar selects con los veh铆culos disponibles |
| cars.forEach(car => { |
| const option1 = document.createElement('option'); |
| option1.value = car.id; |
| option1.textContent = `${car.brand} ${car.model} (${car.plate})`; |
| expenseCarSelect.appendChild(option1); |
| |
| const option2 = document.createElement('option'); |
| option2.value = car.id; |
| option2.textContent = `${car.brand} ${car.model} (${car.plate})`; |
| filterCarSelect.appendChild(option2); |
| }); |
| |
| // Actualizar contador de veh铆culos |
| totalCarsElement.textContent = cars.length; |
| } |
| |
| function updateYearFilter() { |
| // Obtener a帽os 煤nicos de los gastos |
| const years = [...new Set(expenses.map(expense => expense.date.split('-')[0]))]; |
| |
| // Ordenar a帽os de m谩s reciente a m谩s antiguo |
| years.sort((a, b) => b - a); |
| |
| // Limpiar y llenar el select |
| filterYearSelect.innerHTML = '<option value="all">Todos los a帽os</option>'; |
| years.forEach(year => { |
| const option = document.createElement('option'); |
| option.value = year; |
| option.textContent = year; |
| filterYearSelect.appendChild(option); |
| }); |
| } |
| |
| function renderExpenses() { |
| const filteredExpenses = filterExpenses(); |
| |
| // Limpiar tabla |
| expensesTableBody.innerHTML = ''; |
| |
| if (filteredExpenses.length === 0) { |
| expensesTableBody.innerHTML = ` |
| <tr> |
| <td colspan="6" class="px-6 py-4 text-center text-sm text-gray-500">No hay gastos que coincidan con los filtros</td> |
| </tr> |
| `; |
| return; |
| } |
| |
| // Ordenar gastos por fecha (m谩s reciente primero) |
| filteredExpenses.sort((a, b) => new Date(b.date) - new Date(a.date)); |
| |
| // Llenar tabla con los gastos filtrados |
| filteredExpenses.forEach(expense => { |
| const row = document.createElement('tr'); |
| row.className = 'hover:bg-gray-50'; |
| |
| // Formatear fecha |
| const date = new Date(expense.date); |
| const formattedDate = date.toLocaleDateString('es-ES', { |
| day: '2-digit', |
| month: '2-digit', |
| year: 'numeric' |
| }); |
| |
| // Determinar color seg煤n el tipo de gasto |
| let typeColor = 'gray'; |
| switch(expense.type) { |
| case 'Mantenimiento': typeColor = 'blue'; break; |
| case 'Reparaci贸n': typeColor = 'red'; break; |
| case 'Documentos': typeColor = 'green'; break; |
| case 'Seguro': typeColor = 'purple'; break; |
| case 'Combustible': typeColor = 'orange'; break; |
| case 'Otro': typeColor = 'gray'; break; |
| } |
| |
| row.innerHTML = ` |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${formattedDate}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${expense.carBrand} ${expense.carModel}</td> |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-${typeColor}-100 text-${typeColor}-800"> |
| ${expense.type} |
| </span> |
| </td> |
| <td class="px-6 py-4 text-sm text-gray-500 max-w-xs truncate">${expense.description || 'Sin descripci贸n'}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">$${expense.amount.toFixed(2)}</td> |
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> |
| <button class="text-red-600 hover:text-red-900 delete-btn" data-id="${expense.id}"> |
| <i class="fas fa-trash-alt"></i> |
| </button> |
| </td> |
| `; |
| |
| expensesTableBody.appendChild(row); |
| }); |
| |
| // Agregar event listeners a los botones de eliminar |
| document.querySelectorAll('.delete-btn').forEach(btn => { |
| btn.addEventListener('click', function() { |
| expenseToDelete = parseInt(this.getAttribute('data-id')); |
| confirmModal.classList.remove('hidden'); |
| }); |
| }); |
| |
| // Actualizar contador de gastos |
| totalExpensesElement.textContent = filteredExpenses.length; |
| } |
| |
| function filterExpenses() { |
| const carId = filterCarSelect.value === 'all' ? null : parseInt(filterCarSelect.value); |
| const year = filterYearSelect.value === 'all' ? null : filterYearSelect.value; |
| const type = filterTypeSelect.value === 'all' ? null : filterTypeSelect.value; |
| |
| return expenses.filter(expense => { |
| // Filtrar por veh铆culo |
| if (carId !== null && expense.carId !== carId) return false; |
| |
| // Filtrar por a帽o |
| if (year !== null && !expense.date.startsWith(year)) return false; |
| |
| // Filtrar por tipo |
| if (type !== null && expense.type !== type) return false; |
| |
| return true; |
| }); |
| } |
| |
| function updateSummary() { |
| const filteredExpenses = filterExpenses(); |
| |
| // Calcular total gastado |
| const total = filteredExpenses.reduce((sum, expense) => sum + expense.amount, 0); |
| totalSpentElement.textContent = `$${total.toFixed(2)}`; |
| |
| // Actualizar leyenda del gr谩fico |
| let legendText = 'Todos los gastos'; |
| if (filterCarSelect.value !== 'all') { |
| const selectedCar = cars.find(c => c.id === parseInt(filterCarSelect.value)); |
| legendText = `${selectedCar.brand} ${selectedCar.model}`; |
| } |
| if (filterYearSelect.value !== 'all') { |
| legendText += ` - A帽o ${filterYearSelect.value}`; |
| } |
| if (filterTypeSelect.value !== 'all') { |
| legendText += ` - ${filterTypeSelect.value}`; |
| } |
| chartLegend.textContent = legendText; |
| } |
| |
| function renderChart() { |
| const filteredExpenses = filterExpenses(); |
| |
| // Limpiar gr谩fico |
| expenseChart.innerHTML = ''; |
| |
| if (filteredExpenses.length === 0) { |
| expenseChart.innerHTML = '<div class="text-center text-xs text-gray-500">No hay datos para mostrar</div>'; |
| return; |
| } |
| |
| // Agrupar gastos por tipo si no hay filtro de tipo, o por mes si hay filtro de tipo |
| let data = {}; |
| let isGroupedByType = filterTypeSelect.value === 'all'; |
| |
| if (isGroupedByType) { |
| // Agrupar por tipo de gasto |
| filteredExpenses.forEach(expense => { |
| if (!data[expense.type]) { |
| data[expense.type] = 0; |
| } |
| data[expense.type] += expense.amount; |
| }); |
| } else { |
| // Agrupar por mes |
| filteredExpenses.forEach(expense => { |
| const month = expense.date.substring(0, 7); // Formato YYYY-MM |
| if (!data[month]) { |
| data[month] = 0; |
| } |
| data[month] += expense.amount; |
| }); |
| } |
| |
| // Ordenar los datos |
| const sortedData = Object.entries(data).sort((a, b) => { |
| if (isGroupedByType) return a[0].localeCompare(b[0]); |
| return a[0].localeCompare(b[0]); |
| }); |
| |
| // Calcular el m谩ximo valor para escalar las barras |
| const maxValue = Math.max(...Object.values(data)); |
| |
| // Generar las barras del gr谩fico |
| sortedData.forEach(([label, value]) => { |
| const barHeight = maxValue > 0 ? (value / maxValue * 100) : 0; |
| const barColor = getRandomColor(); |
| |
| const barContainer = document.createElement('div'); |
| barContainer.className = 'flex flex-col items-center w-full'; |
| |
| // Formatear el label para meses |
| let displayLabel = label; |
| if (!isGroupedByType) { |
| const [year, month] = label.split('-'); |
| displayLabel = new Date(year, month-1).toLocaleDateString('es-ES', { month: 'short' }); |
| } |
| |
| barContainer.innerHTML = ` |
| <div class="w-8 bg-${barColor}-500 rounded-t-md hover:bg-${barColor}-600 transition duration-200" style="height: ${barHeight}%"></div> |
| <div class="text-xs text-gray-500 mt-1 text-center">${displayLabel}</div> |
| <div class="text-xs font-medium mt-1">$${value.toFixed(2)}</div> |
| `; |
| |
| expenseChart.appendChild(barContainer); |
| }); |
| } |
| |
| function getRandomColor() { |
| const colors = ['indigo', 'blue', 'green', 'yellow', 'red', 'purple', 'pink', 'orange']; |
| return colors[Math.floor(Math.random() * colors.length)]; |
| } |
| |
| function saveData() { |
| localStorage.setItem('cars', JSON.stringify(cars)); |
| localStorage.setItem('expenses', JSON.stringify(expenses)); |
| } |
| |
| function exportData() { |
| const filteredExpenses = filterExpenses(); |
| |
| if (filteredExpenses.length === 0) { |
| showNotification('No hay datos para exportar', 'warning'); |
| return; |
| } |
| |
| // Crear contenido CSV |
| let csvContent = "Fecha,Veh铆culo,Tipo,Descripci贸n,Monto\n"; |
| |
| filteredExpenses.forEach(expense => { |
| const date = new Date(expense.date).toLocaleDateString('es-ES'); |
| const carInfo = `${expense.carBrand} ${expense.carModel} (${expense.carPlate})`; |
| const description = expense.description ? `"${expense.description.replace(/"/g, '""')}"` : ''; |
| |
| csvContent += `${date},${carInfo},${expense.type},${description},${expense.amount.toFixed(2)}\n`; |
| }); |
| |
| // Crear y descargar archivo |
| const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); |
| const url = URL.createObjectURL(blob); |
| const link = document.createElement('a'); |
| link.setAttribute('href', url); |
| link.setAttribute('download', `gastos_vehiculos_${new Date().toISOString().slice(0, 10)}.csv`); |
| link.style.visibility = 'hidden'; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
| |
| // Mostrar notificaci贸n |
| showNotification('Datos exportados correctamente', 'success'); |
| } |
| |
| function showNotification(message, type) { |
| const notification = document.createElement('div'); |
| notification.className = `fixed bottom-4 right-4 px-4 py-2 rounded-md shadow-md text-white ${ |
| type === 'success' ? 'bg-green-500' : |
| type === 'error' ? 'bg-red-500' : |
| 'bg-yellow-500' |
| } flex items-center`; |
| |
| notification.innerHTML = ` |
| <i class="fas ${ |
| type === 'success' ? 'fa-check-circle' : |
| type === 'error' ? 'fa-exclamation-circle' : |
| 'fa-exclamation-triangle' |
| } mr-2"></i> |
| <span>${message}</span> |
| `; |
| |
| document.body.appendChild(notification); |
| |
| // Desvanecer y eliminar la notificaci贸n despu茅s de 3 segundos |
| setTimeout(() => { |
| notification.style.opacity = '0'; |
| notification.style.transition = 'opacity 0.5s ease-out'; |
| setTimeout(() => { |
| notification.remove(); |
| }, 500); |
| }, 3000); |
| } |
| </script> |
| <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> |
| </html> |