| <!DOCTYPE html> |
| <html lang="pt"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Água Sonho Real - Sistema de Cobrança</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"> |
| <script> |
| tailwind.config = { |
| theme: { |
| extend: { |
| colors: { |
| primary: '#3498db', |
| secondary: '#2980b9', |
| danger: '#e74c3c', |
| warning: '#f39c12', |
| success: '#2ecc71', |
| dark: '#2c3e50', |
| } |
| } |
| } |
| } |
| </script> |
| <style> |
| .sidebar { |
| transition: all 0.3s; |
| } |
| @media (max-width: 768px) { |
| .sidebar { |
| transform: translateX(-100%); |
| } |
| .sidebar.active { |
| transform: translateX(0); |
| } |
| } |
| .tab-content { |
| display: none; |
| } |
| .tab-content.active { |
| display: block; |
| } |
| .alert-pulse { |
| animation: pulse 2s infinite; |
| } |
| @keyframes pulse { |
| 0% { |
| box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.4); |
| } |
| 70% { |
| box-shadow: 0 0 0 10px rgba(231, 76, 60, 0); |
| } |
| 100% { |
| box-shadow: 0 0 0 0 rgba(231, 76, 60, 0); |
| } |
| } |
| </style> |
| </head> |
| <body class="bg-gray-100 font-sans"> |
| <div class="flex h-screen overflow-hidden"> |
| |
| <div class="sidebar bg-dark text-white w-64 fixed h-full z-10"> |
| <div class="p-4 flex items-center space-x-2 border-b border-gray-700"> |
| <i class="fas fa-tint text-2xl text-primary"></i> |
| <h1 class="text-xl font-bold">Água Sonho Real</h1> |
| </div> |
| <nav class="p-4"> |
| <ul class="space-y-2"> |
| <li> |
| <button onclick="showTab('dashboard')" class="tab-btn w-full text-left p-2 rounded hover:bg-secondary flex items-center space-x-2"> |
| <i class="fas fa-home"></i> |
| <span>Dashboard</span> |
| </button> |
| </li> |
| <li> |
| <button onclick="showTab('billing')" class="tab-btn w-full text-left p-2 rounded hover:bg-secondary flex items-center space-x-2"> |
| <i class="fas fa-file-invoice-dollar"></i> |
| <span>Cobrança</span> |
| </button> |
| </li> |
| <li> |
| <button onclick="showTab('customers')" class="tab-btn w-full text-left p-2 rounded hover:bg-secondary flex items-center space-x-2"> |
| <i class="fas fa-users"></i> |
| <span>Clientes</span> |
| </button> |
| </li> |
| <li> |
| <button onclick="showTab('reports')" class="tab-btn w-full text-left p-2 rounded hover:bg-secondary flex items-center space-x-2"> |
| <i class="fas fa-chart-bar"></i> |
| <span>Relatórios</span> |
| </button> |
| </li> |
| <li> |
| <button onclick="showTab('settings')" class="tab-btn w-full text-left p-2 rounded hover:bg-secondary flex items-center space-x-2"> |
| <i class="fas fa-cog"></i> |
| <span>Configurações</span> |
| </button> |
| </li> |
| </ul> |
| </nav> |
| <div class="absolute bottom-0 w-full p-4 border-t border-gray-700"> |
| <button class="w-full bg-primary hover:bg-secondary text-white p-2 rounded flex items-center justify-center space-x-2"> |
| <i class="fas fa-sign-out-alt"></i> |
| <span>Sair</span> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="flex-1 ml-0 md:ml-64 transition-all duration-300"> |
| |
| <header class="bg-white shadow md:hidden p-4 flex items-center justify-between"> |
| <button id="menu-toggle" class="text-dark"> |
| <i class="fas fa-bars text-xl"></i> |
| </button> |
| <h1 class="text-xl font-bold text-dark">Água Sonho Real</h1> |
| <div class="w-8"></div> |
| </header> |
|
|
| |
| <main class="p-4 md:p-6"> |
| |
| <div id="dashboard" class="tab-content active"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-2xl font-bold text-dark">Dashboard</h2> |
| <div class="text-sm text-gray-500"> |
| <span id="current-date"></span> |
| </div> |
| </div> |
|
|
| |
| <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6"> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <p class="text-gray-500">Clientes Ativos</p> |
| <h3 class="text-2xl font-bold" id="active-customers">0</h3> |
| </div> |
| <div class="bg-primary bg-opacity-10 p-3 rounded-full"> |
| <i class="fas fa-users text-primary text-xl"></i> |
| </div> |
| </div> |
| </div> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <p class="text-gray-500">Receita Mensal</p> |
| <h3 class="text-2xl font-bold" id="monthly-revenue">0 MZN</h3> |
| </div> |
| <div class="bg-success bg-opacity-10 p-3 rounded-full"> |
| <i class="fas fa-money-bill-wave text-success text-xl"></i> |
| </div> |
| </div> |
| </div> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <p class="text-gray-500">Pagamentos Atrasados</p> |
| <h3 class="text-2xl font-bold" id="late-payments">0</h3> |
| </div> |
| <div class="bg-danger bg-opacity-10 p-3 rounded-full"> |
| <i class="fas fa-exclamation-triangle text-danger text-xl"></i> |
| </div> |
| </div> |
| </div> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <div class="flex items-center justify-between"> |
| <div> |
| <p class="text-gray-500">Consumo Médio</p> |
| <h3 class="text-2xl font-bold" id="avg-consumption">0 m³</h3> |
| </div> |
| <div class="bg-secondary bg-opacity-10 p-3 rounded-full"> |
| <i class="fas fa-tint text-secondary text-xl"></i> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="mb-6"> |
| <h3 class="text-lg font-semibold mb-2 text-dark">Alertas de Pagamento</h3> |
| <div id="alerts-container" class="space-y-2"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-lg shadow overflow-hidden"> |
| <div class="p-4 border-b"> |
| <h3 class="text-lg font-semibold text-dark">Últimos Pagamentos</h3> |
| </div> |
| <div class="overflow-x-auto"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cliente</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nº Cliente</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Área</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> |
| </tr> |
| </thead> |
| <tbody id="recent-payments" class="bg-white divide-y divide-gray-200"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="billing" class="tab-content"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-2xl font-bold text-dark">Cobrança</h2> |
| <button onclick="showNewBillingModal()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded flex items-center space-x-2"> |
| <i class="fas fa-plus"></i> |
| <span>Nova Cobrança</span> |
| </button> |
| </div> |
|
|
| <div class="bg-white rounded-lg shadow overflow-hidden mb-6"> |
| <div class="p-4 border-b"> |
| <h3 class="text-lg font-semibold text-dark">Registrar Pagamento</h3> |
| </div> |
| <div class="p-4"> |
| <form id="payment-form" class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
| <div> |
| <label for="customer-search" class="block text-sm font-medium text-gray-700 mb-1">Buscar Cliente</label> |
| <div class="relative"> |
| <input type="text" id="customer-search" class="w-full p-2 border border-gray-300 rounded" placeholder="Nome ou Nº Cliente"> |
| <div id="search-results" class="absolute z-10 w-full mt-1 bg-white border border-gray-300 rounded shadow-lg hidden"></div> |
| </div> |
| </div> |
| <div> |
| <label for="consumption" class="block text-sm font-medium text-gray-700 mb-1">Consumo (m³)</label> |
| <input type="number" id="consumption" min="5" step="0.1" class="w-full p-2 border border-gray-300 rounded" value="5"> |
| </div> |
| <div> |
| <label for="amount" class="block text-sm font-medium text-gray-700 mb-1">Valor (MZN)</label> |
| <input type="number" id="amount" class="w-full p-2 border border-gray-300 rounded" readonly> |
| </div> |
| <div class="md:col-span-3"> |
| <button type="button" onclick="calculateAmount()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded mr-2"> |
| Calcular |
| </button> |
| <button type="button" onclick="registerPayment()" class="bg-success hover:bg-green-600 text-white px-4 py-2 rounded"> |
| Registrar Pagamento |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| <div class="bg-white rounded-lg shadow overflow-hidden"> |
| <div class="p-4 border-b flex justify-between items-center"> |
| <h3 class="text-lg font-semibold text-dark">Histórico de Cobranças</h3> |
| <div class="flex space-x-2"> |
| <select id="billing-filter" class="p-2 border border-gray-300 rounded"> |
| <option value="all">Todos</option> |
| <option value="paid">Pagas</option> |
| <option value="pending">Pendentes</option> |
| <option value="overdue">Atrasadas</option> |
| </select> |
| <input type="month" id="billing-month" class="p-2 border border-gray-300 rounded"> |
| </div> |
| </div> |
| <div class="overflow-x-auto"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nº Cliente</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cliente</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Área</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Consumo</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th> |
| </tr> |
| </thead> |
| <tbody id="billing-history" class="bg-white divide-y divide-gray-200"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="customers" class="tab-content"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-2xl font-bold text-dark">Clientes</h2> |
| <button onclick="showNewCustomerModal()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded flex items-center space-x-2"> |
| <i class="fas fa-plus"></i> |
| <span>Novo Cliente</span> |
| </button> |
| </div> |
|
|
| <div class="bg-white rounded-lg shadow overflow-hidden mb-6"> |
| <div class="p-4 border-b flex justify-between items-center"> |
| <h3 class="text-lg font-semibold text-dark">Lista de Clientes</h3> |
| <div class="flex space-x-2"> |
| <select id="customer-area-filter" class="p-2 border border-gray-300 rounded"> |
| <option value="all">Todas Áreas</option> |
| <option value="1">Possulane</option> |
| <option value="2">Condomínios</option> |
| </select> |
| <input type="text" id="customer-search-main" placeholder="Buscar cliente..." class="p-2 border border-gray-300 rounded"> |
| </div> |
| </div> |
| <div class="overflow-x-auto"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nº Cliente</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nome</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Área</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Contacto</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Último Pagamento</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ações</th> |
| </tr> |
| </thead> |
| <tbody id="customers-list" class="bg-white divide-y divide-gray-200"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="reports" class="tab-content"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-2xl font-bold text-dark">Relatórios</h2> |
| <div class="flex space-x-2"> |
| <button onclick="generatePDFReport()" class="bg-danger hover:bg-red-600 text-white px-4 py-2 rounded flex items-center space-x-2"> |
| <i class="fas fa-file-pdf"></i> |
| <span>Exportar PDF</span> |
| </button> |
| <button onclick="generateExcelReport()" class="bg-success hover:bg-green-600 text-white px-4 py-2 rounded flex items-center space-x-2"> |
| <i class="fas fa-file-excel"></i> |
| <span>Exportar Excel</span> |
| </button> |
| </div> |
| </div> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-semibold mb-4 text-dark">Filtros do Relatório</h3> |
| <form id="report-filters" class="space-y-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Tipo de Relatório</label> |
| <select id="report-type" class="w-full p-2 border border-gray-300 rounded"> |
| <option value="monthly">Mensal</option> |
| <option value="quarterly">Trimestral</option> |
| <option value="annual">Anual</option> |
| <option value="custom">Personalizado</option> |
| </select> |
| </div> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div> |
| <label for="start-date" class="block text-sm font-medium text-gray-700 mb-1">Data Inicial</label> |
| <input type="date" id="start-date" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| <div> |
| <label for="end-date" class="block text-sm font-medium text-gray-700 mb-1">Data Final</label> |
| <input type="date" id="end-date" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Área</label> |
| <select id="report-area" class="w-full p-2 border border-gray-300 rounded"> |
| <option value="all">Todas Áreas</option> |
| <option value="1">Possulane</option> |
| <option value="2">Condomínios</option> |
| </select> |
| </div> |
| <button type="button" onclick="generateReport()" class="w-full bg-primary hover:bg-secondary text-white px-4 py-2 rounded"> |
| Gerar Relatório |
| </button> |
| </form> |
| </div> |
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-semibold mb-4 text-dark">Resumo Financeiro</h3> |
| <div class="space-y-4"> |
| <div> |
| <h4 class="font-medium text-gray-700">Receita Total</h4> |
| <p class="text-2xl font-bold" id="total-revenue">0 MZN</p> |
| </div> |
| <div> |
| <h4 class="font-medium text-gray-700">Pagamentos Pendentes</h4> |
| <p class="text-2xl font-bold text-danger" id="pending-revenue">0 MZN</p> |
| </div> |
| <div> |
| <h4 class="font-medium text-gray-700">Número de Clientes</h4> |
| <div class="flex justify-between"> |
| <div> |
| <p class="text-sm text-gray-500">Possulane</p> |
| <p class="text-xl font-bold" id="possulane-customers">0</p> |
| </div> |
| <div> |
| <p class="text-sm text-gray-500">Condomínios</p> |
| <p class="text-xl font-bold" id="condominios-customers">0</p> |
| </div> |
| <div> |
| <p class="text-sm text-gray-500">Total</p> |
| <p class="text-xl font-bold" id="total-customers">0</p> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bg-white rounded-lg shadow overflow-hidden"> |
| <div class="p-4 border-b"> |
| <h3 class="text-lg font-semibold text-dark">Resultados do Relatório</h3> |
| </div> |
| <div class="p-4"> |
| <div class="overflow-x-auto"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Mês</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Receita (MZN)</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Clientes Ativos</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Pagamentos Atrasados</th> |
| <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Consumo Médio (m³)</th> |
| </tr> |
| </thead> |
| <tbody id="report-results" class="bg-white divide-y divide-gray-200"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="settings" class="tab-content"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-2xl font-bold text-dark">Configurações</h2> |
| </div> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| <div class="bg-white p-4 rounded-lg shadow lg:col-span-2"> |
| <h3 class="text-lg font-semibold mb-4 text-dark">Configurações de Cobrança</h3> |
| <form id="billing-settings" class="space-y-4"> |
| <div> |
| <label for="min-consumption" class="block text-sm font-medium text-gray-700 mb-1">Consumo Mínimo (m³)</label> |
| <input type="number" id="min-consumption" min="1" step="1" class="w-full p-2 border border-gray-300 rounded" value="5"> |
| </div> |
| <div> |
| <label for="min-consumption-price" class="block text-sm font-medium text-gray-700 mb-1">Preço Consumo Mínimo (MZN)</label> |
| <input type="number" id="min-consumption-price" min="1" step="1" class="w-full p-2 border border-gray-300 rounded" value="300"> |
| </div> |
| <div> |
| <label for="extra-consumption-price" class="block text-sm font-medium text-gray-700 mb-1">Preço por m³ adicional (MZN)</label> |
| <input type="number" id="extra-consumption-price" min="1" step="1" class="w-full p-2 border border-gray-300 rounded" value="60"> |
| </div> |
| <div> |
| <label for="due-date" class="block text-sm font-medium text-gray-700 mb-1">Dia de Vencimento</label> |
| <input type="number" id="due-date" min="1" max="31" class="w-full p-2 border border-gray-300 rounded" value="15"> |
| </div> |
| <div> |
| <label for="late-fee" class="block text-sm font-medium text-gray-700 mb-1">Multa por Atraso (%)</label> |
| <input type="number" id="late-fee" min="0" max="100" step="0.1" class="w-full p-2 border border-gray-300 rounded" value="5"> |
| </div> |
| <button type="button" onclick="saveBillingSettings()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded"> |
| Salvar Configurações |
| </button> |
| </form> |
| </div> |
|
|
| <div class="bg-white p-4 rounded-lg shadow"> |
| <h3 class="text-lg font-semibold mb-4 text-dark">Configurações de Usuário</h3> |
| <form id="user-settings" class="space-y-4"> |
| <div> |
| <label for="username" class="block text-sm font-medium text-gray-700 mb-1">Nome de Usuário</label> |
| <input type="text" id="username" class="w-full p-2 border border-gray-300 rounded" value="admin"> |
| </div> |
| <div> |
| <label for="email" class="block text-sm font-medium text-gray-700 mb-1">Email</label> |
| <input type="email" id="email" class="w-full p-2 border border-gray-300 rounded" value="admin@aguasonhoreal.com"> |
| </div> |
| <div> |
| <label for="current-password" class="block text-sm font-medium text-gray-700 mb-1">Senha Atual</label> |
| <input type="password" id="current-password" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| <div> |
| <label for="new-password" class="block text-sm font-medium text-gray-700 mb-1">Nova Senha</label> |
| <input type="password" id="new-password" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| <div> |
| <label for="confirm-password" class="block text-sm font-medium text-gray-700 mb-1">Confirmar Nova Senha</label> |
| <input type="password" id="confirm-password" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| <button type="button" onclick="saveUserSettings()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded"> |
| Salvar Alterações |
| </button> |
| </form> |
| </div> |
| </div> |
| </div> |
| </main> |
| </div> |
| </div> |
|
|
| |
| |
| <div id="new-customer-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg shadow-lg w-full max-w-md"> |
| <div class="p-4 border-b"> |
| <h3 class="text-lg font-semibold text-dark">Novo Cliente</h3> |
| </div> |
| <div class="p-4"> |
| <form id="new-customer-form" class="space-y-4"> |
| <div> |
| <label for="customer-name" class="block text-sm font-medium text-gray-700 mb-1">Nome Completo</label> |
| <input type="text" id="customer-name" class="w-full p-2 border border-gray-300 rounded" required> |
| </div> |
| <div> |
| <label for="customer-area" class="block text-sm font-medium text-gray-700 mb-1">Área</label> |
| <select id="customer-area" class="w-full p-2 border border-gray-300 rounded" required> |
| <option value="1">Possulane</option> |
| <option value="2">Condomínios</option> |
| </select> |
| </div> |
| <div> |
| <label for="customer-address" class="block text-sm font-medium text-gray-700 mb-1">Endereço</label> |
| <input type="text" id="customer-address" class="w-full p-2 border border-gray-300 rounded" required> |
| </div> |
| <div> |
| <label for="customer-phone" class="block text-sm font-medium text-gray-700 mb-1">Telefone</label> |
| <input type="tel" id="customer-phone" class="w-full p-2 border border-gray-300 rounded" required> |
| </div> |
| <div> |
| <label for="customer-email" class="block text-sm font-medium text-gray-700 mb-1">Email (Opcional)</label> |
| <input type="email" id="customer-email" class="w-full p-2 border border-gray-300 rounded"> |
| </div> |
| </form> |
| </div> |
| <div class="p-4 border-t flex justify-end space-x-2"> |
| <button onclick="hideModal('new-customer-modal')" class="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-100"> |
| Cancelar |
| </button> |
| <button onclick="saveNewCustomer()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded"> |
| Salvar Cliente |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="customer-details-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg shadow-lg w-full max-w-md"> |
| <div class="p-4 border-b flex justify-between items-center"> |
| <h3 class="text-lg font-semibold text-dark">Detalhes do Cliente</h3> |
| <button onclick="hideModal('customer-details-modal')" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div class="p-4"> |
| <div class="mb-4"> |
| <h4 class="font-bold text-lg" id="detail-customer-name"></h4> |
| <p class="text-gray-600" id="detail-customer-number"></p> |
| </div> |
| <div class="space-y-3"> |
| <div> |
| <p class="text-sm text-gray-500">Área</p> |
| <p id="detail-customer-area"></p> |
| </div> |
| <div> |
| <p class="text-sm text-gray-500">Endereço</p> |
| <p id="detail-customer-address"></p> |
| </div> |
| <div> |
| <p class="text-sm text-gray-500">Telefone</p> |
| <p id="detail-customer-phone"></p> |
| </div> |
| <div> |
| <p class="text-sm text-gray-500">Email</p> |
| <p id="detail-customer-email"></p> |
| </div> |
| <div> |
| <p class="text-sm text-gray-500">Data de Registro</p> |
| <p id="detail-customer-register-date"></p> |
| </div> |
| </div> |
| </div> |
| <div class="p-4 border-t"> |
| <h4 class="font-semibold mb-2">Histórico de Pagamentos</h4> |
| <div class="overflow-y-auto max-h-40"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Data</th> |
| <th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Valor</th> |
| <th class="px-2 py-1 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> |
| </tr> |
| </thead> |
| <tbody id="customer-payment-history" class="bg-white divide-y divide-gray-200"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
| <div class="p-4 border-t flex justify-end space-x-2"> |
| <button onclick="hideModal('customer-details-modal')" class="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-100"> |
| Fechar |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="new-billing-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg shadow-lg w-full max-w-md"> |
| <div class="p-4 border-b"> |
| <h3 class="text-lg font-semibold text-dark">Nova Cobrança</h3> |
| </div> |
| <div class="p-4"> |
| <form id="new-billing-form" class="space-y-4"> |
| <div> |
| <label for="billing-customer" class="block text-sm font-medium text-gray-700 mb-1">Cliente</label> |
| <select id="billing-customer" class="w-full p-2 border border-gray-300 rounded" required> |
| |
| </select> |
| </div> |
| <div> |
| <label for="billing-month" class="block text-sm font-medium text-gray-700 mb-1">Mês de Referência</label> |
| <input type="month" id="billing-month" class="w-full p-2 border border-gray-300 rounded" required> |
| </div> |
| <div> |
| <label for="billing-consumption" class="block text-sm font-medium text-gray-700 mb-1">Consumo (m³)</label> |
| <input type="number" id="billing-consumption" min="5" step="0.1" class="w-full p-2 border border-gray-300 rounded" value="5" required> |
| </div> |
| <div> |
| <label for="billing-amount" class="block text-sm font-medium text-gray-700 mb-1">Valor (MZN)</label> |
| <input type="number" id="billing-amount" class="w-full p-2 border border-gray-300 rounded" readonly> |
| </div> |
| <div> |
| <label for="billing-due-date" class="block text-sm font-medium text-gray-700 mb-1">Data de Vencimento</label> |
| <input type="date" id="billing-due-date" class="w-full p-2 border border-gray-300 rounded" required> |
| </div> |
| </form> |
| </div> |
| <div class="p-4 border-t flex justify-end space-x-2"> |
| <button onclick="hideModal('new-billing-modal')" class="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-100"> |
| Cancelar |
| </button> |
| <button onclick="saveNewBilling()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded"> |
| Salvar Cobrança |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="payment-receipt-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg shadow-lg w-full max-w-md"> |
| <div class="p-4 border-b flex justify-between items-center"> |
| <h3 class="text-lg font-semibold text-dark">Recibo de Pagamento</h3> |
| <button onclick="hideModal('payment-receipt-modal')" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| <div class="p-4"> |
| <div class="text-center mb-4"> |
| <h2 class="text-xl font-bold">Água Sonho Real</h2> |
| <p class="text-sm text-gray-600">Sistema de Cobrança</p> |
| </div> |
| <div class="border-b pb-4 mb-4"> |
| <div class="flex justify-between mb-2"> |
| <span class="font-semibold">Nº Recibo:</span> |
| <span id="receipt-number"></span> |
| </div> |
| <div class="flex justify-between mb-2"> |
| <span class="font-semibold">Data:</span> |
| <span id="receipt-date"></span> |
| </div> |
| <div class="flex justify-between"> |
| <span class="font-semibold">Cliente:</span> |
| <span id="receipt-customer"></span> |
| </div> |
| </div> |
| <div class="mb-4"> |
| <table class="w-full"> |
| <thead> |
| <tr class="border-b"> |
| <th class="text-left py-2">Descrição</th> |
| <th class="text-right py-2">Valor</th> |
| </tr> |
| </thead> |
| <tbody> |
| <tr class="border-b"> |
| <td class="py-2">Consumo de Água</td> |
| <td class="text-right py-2" id="receipt-water-amount"></td> |
| </tr> |
| <tr class="border-b"> |
| <td class="py-2">Multa por Atraso</td> |
| <td class="text-right py-2" id="receipt-late-fee"></td> |
| </tr> |
| <tr class="font-bold"> |
| <td class="py-2">Total</td> |
| <td class="text-right py-2" id="receipt-total"></td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| <div class="text-center text-sm text-gray-500"> |
| <p>Obrigado pelo seu pagamento!</p> |
| <p>Para dúvidas, contacte: +258 84 123 4567</p> |
| </div> |
| </div> |
| <div class="p-4 border-t flex justify-end space-x-2"> |
| <button onclick="printReceipt()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded flex items-center space-x-2"> |
| <i class="fas fa-print"></i> |
| <span>Imprimir</span> |
| </button> |
| <button onclick="hideModal('payment-receipt-modal')" class="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-100"> |
| Fechar |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="confirmation-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg shadow-lg w-full max-w-md"> |
| <div class="p-4 border-b"> |
| <h3 class="text-lg font-semibold text-dark" id="confirmation-title"></h3> |
| </div> |
| <div class="p-4"> |
| <p id="confirmation-message"></p> |
| </div> |
| <div class="p-4 border-t flex justify-end space-x-2"> |
| <button onclick="hideModal('confirmation-modal')" class="px-4 py-2 border border-gray-300 rounded text-gray-700 hover:bg-gray-100"> |
| Cancelar |
| </button> |
| <button onclick="confirmAction()" class="bg-primary hover:bg-secondary text-white px-4 py-2 rounded" id="confirm-button"> |
| Confirmar |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let customers = [ |
| { id: 1, number: '1-001', name: 'João Matola', area: '1', areaName: 'Possulane', address: 'Av. Marginal, 123', phone: '841234567', email: 'joao@email.com', registrationDate: '2023-01-15', status: 'active' }, |
| { id: 2, number: '1-002', name: 'Maria Nhantumbo', area: '1', areaName: 'Possulane', address: 'Rua da Escola, 45', phone: '842345678', email: 'maria@email.com', registrationDate: '2023-02-10', status: 'active' }, |
| { id: 3, number: '2-001', name: 'Condomínio Flor do Mar', area: '2', areaName: 'Condomínios', address: 'Av. da Praia, 300', phone: '843456789', email: 'flordomar@email.com', registrationDate: '2023-01-05', status: 'active' }, |
| { id: 4, number: '2-002', name: 'Condomínio Vista Linda', area: '2', areaName: 'Condomínios', address: 'Rua dos Coqueiros, 12', phone: '844567890', email: 'vistalinda@email.com', registrationDate: '2023-03-20', status: 'active' }, |
| ]; |
| |
| let bills = [ |
| { id: 1, customerId: 1, customerNumber: '1-001', customerName: 'João Matola', area: '1', areaName: 'Possulane', consumption: 5, amount: 300, date: '2023-06-15', dueDate: '2023-07-15', paymentDate: '2023-07-10', status: 'paid', lateFee: 0 }, |
| { id: 2, customerId: 2, customerNumber: '1-002', customerName: 'Maria Nhantumbo', area: '1', areaName: 'Possulane', consumption: 7, amount: 420, date: '2023-06-15', dueDate: '2023-07-15', paymentDate: '2023-07-18', status: 'paid', lateFee: 21 }, |
| { id: 3, customerId: 3, customerNumber: '2-001', customerName: 'Condomínio Flor do Mar', area: '2', areaName: 'Condomínios', consumption: 15, amount: 900, date: '2023-06-15', dueDate: '2023-07-15', paymentDate: null, status: 'overdue', lateFee: 45 }, |
| { id: 4, customerId: 4, customerNumber: '2-002', customerName: 'Condomínio Vista Linda', area: '2', areaName: 'Condomínios', consumption: 8, amount: 480, date: '2023-06-15', dueDate: '2023-07-15', paymentDate: null, status: 'pending', lateFee: 0 }, |
| { id: 5, customerId: 1, customerNumber: '1-001', customerName: 'João Matola', area: '1', areaName: 'Possulane', consumption: 6, amount: 360, date: '2023-05-15', dueDate: '2023-06-15', paymentDate: '2023-06-12', status: 'paid', lateFee: 0 }, |
| { id: 6, customerId: 2, customerNumber: '1-002', customerName: 'Maria Nhantumbo', area: '1', areaName: 'Possulane', consumption: 5, amount: 300, date: '2023-05-15', dueDate: '2023-06-15', paymentDate: '2023-06-20', status: 'paid', lateFee: 15 }, |
| { id: 7, customerId: 3, customerNumber: '2-001', customerName: 'Condomínio Flor do Mar', area: '2', areaName: 'Condomínios', consumption: 12, amount: 720, date: '2023-05-15', dueDate: '2023-06-15', paymentDate: '2023-06-10', status: 'paid', lateFee: 0 }, |
| { id: 8, customerId: 4, customerNumber: '2-002', customerName: 'Condomínio Vista Linda', area: '2', areaName: 'Condomínios', consumption: 7, amount: 420, date: '2023-05-15', dueDate: '2023-06-15', paymentDate: '2023-06-14', status: 'paid', lateFee: 0 }, |
| { id: 9, customerId: 1, customerNumber: '1-001', customerName: 'João Matola', area: '1', areaName: 'Possulane', consumption: 5, amount: 300, date: '2023-04-15', dueDate: '2023-05-15', paymentDate: '2023-05-10', status: 'paid', lateFee: 0 }, |
| { id: 10, customerId: 2, customerNumber: '1-002', customerName: 'Maria Nhantumbo', area: '1', areaName: 'Possulane', consumption: 9, amount: 540, date: '2023-04-15', dueDate: '2023-05-15', paymentDate: '2023-05-18', status: 'paid', lateFee: 27 }, |
| ]; |
| |
| let settings = { |
| minConsumption: 5, |
| minConsumptionPrice: 300, |
| extraConsumptionPrice: 60, |
| dueDate: 15, |
| lateFee: 5 |
| }; |
| |
| |
| let currentAction = null; |
| let currentActionData = null; |
| |
| |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const now = new Date(); |
| document.getElementById('current-date').textContent = now.toLocaleDateString('pt-PT', { |
| weekday: 'long', |
| year: 'numeric', |
| month: 'long', |
| day: 'numeric' |
| }); |
| |
| |
| updateDashboard(); |
| loadRecentPayments(); |
| loadAlerts(); |
| |
| |
| calculateAmount(); |
| loadBillingHistory(); |
| |
| |
| loadCustomersList(); |
| |
| |
| updateReportSummary(); |
| |
| |
| loadSettings(); |
| |
| |
| document.getElementById('menu-toggle').addEventListener('click', function() { |
| document.querySelector('.sidebar').classList.toggle('active'); |
| }); |
| |
| |
| document.getElementById('customer-search').addEventListener('input', function() { |
| searchCustomers(this.value); |
| }); |
| |
| |
| document.getElementById('customer-search-main').addEventListener('input', function() { |
| filterCustomers(); |
| }); |
| |
| |
| document.getElementById('billing-filter').addEventListener('change', function() { |
| loadBillingHistory(); |
| }); |
| |
| |
| document.getElementById('billing-month').addEventListener('change', function() { |
| loadBillingHistory(); |
| }); |
| |
| |
| document.getElementById('customer-area-filter').addEventListener('change', function() { |
| filterCustomers(); |
| }); |
| }); |
| |
| |
| function showTab(tabId) { |
| document.querySelectorAll('.tab-content').forEach(tab => { |
| tab.classList.remove('active'); |
| }); |
| document.getElementById(tabId).classList.add('active'); |
| |
| |
| document.querySelector('.sidebar').classList.remove('active'); |
| } |
| |
| |
| function showModal(modalId) { |
| document.getElementById(modalId).classList.remove('hidden'); |
| } |
| |
| function hideModal(modalId) { |
| document.getElementById(modalId).classList.add('hidden'); |
| } |
| |
| function showNewCustomerModal() { |
| document.getElementById('customer-name').value = ''; |
| document.getElementById('customer-area').value = '1'; |
| document.getElementById('customer-address').value = ''; |
| document.getElementById('customer-phone').value = ''; |
| document.getElementById('customer-email').value = ''; |
| showModal('new-customer-modal'); |
| } |
| |
| function showNewBillingModal() { |
| const billingCustomerSelect = document.getElementById('billing-customer'); |
| billingCustomerSelect.innerHTML = ''; |
| |
| customers.forEach(customer => { |
| const option = document.createElement('option'); |
| option.value = customer.id; |
| option.textContent = `${customer.number} - ${customer.name}`; |
| billingCustomerSelect.appendChild(option); |
| }); |
| |
| const now = new Date(); |
| const currentMonth = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0'); |
| document.getElementById('billing-month').value = currentMonth; |
| document.getElementById('billing-consumption').value = '5'; |
| document.getElementById('billing-amount').value = '300'; |
| |
| |
| const nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1); |
| const dueDate = new Date(nextMonth.getFullYear(), nextMonth.getMonth(), settings.dueDate); |
| document.getElementById('billing-due-date').value = dueDate.toISOString().split('T')[0]; |
| |
| showModal('new-billing-modal'); |
| } |
| |
| function showCustomerDetails(customerId) { |
| const customer = customers.find(c => c.id == customerId); |
| if (!customer) return; |
| |
| document.getElementById('detail-customer-name').textContent = customer.name; |
| document.getElementById('detail-customer-number').textContent = `Nº Cliente: ${customer.number}`; |
| document.getElementById('detail-customer-area').textContent = customer.areaName; |
| document.getElementById('detail-customer-address').textContent = customer.address; |
| document.getElementById('detail-customer-phone').textContent = customer.phone; |
| document.getElementById('detail-customer-email').textContent = customer.email || 'N/A'; |
| document.getElementById('detail-customer-register-date').textContent = new Date(customer.registrationDate).toLocaleDateString('pt-PT'); |
| |
| |
| const paymentHistory = bills.filter(bill => bill.customerId == customerId) |
| .sort((a, b) => new Date(b.date) - new Date(a.date)); |
| |
| const paymentHistoryTable = document.getElementById('customer-payment-history'); |
| paymentHistoryTable.innerHTML = ''; |
| |
| paymentHistory.forEach(bill => { |
| const row = document.createElement('tr'); |
| |
| const dateCell = document.createElement('td'); |
| dateCell.className = 'px-2 py-1 whitespace-nowrap'; |
| dateCell.textContent = new Date(bill.date).toLocaleDateString('pt-PT'); |
| |
| const amountCell = document.createElement('td'); |
| amountCell.className = 'px-2 py-1 whitespace-nowrap'; |
| amountCell.textContent = `${bill.amount} MZN`; |
| |
| const statusCell = document.createElement('td'); |
| statusCell.className = 'px-2 py-1 whitespace-nowrap'; |
| |
| const statusBadge = document.createElement('span'); |
| statusBadge.className = 'px-2 py-1 text-xs rounded-full'; |
| |
| if (bill.status === 'paid') { |
| statusBadge.classList.add('bg-green-100', 'text-green-800'); |
| statusBadge.textContent = 'Pago'; |
| } else if (bill.status === 'overdue') { |
| statusBadge.classList.add('bg-red-100', 'text-red-800'); |
| statusBadge.textContent = 'Atrasado'; |
| } else { |
| statusBadge.classList.add('bg-yellow-100', 'text-yellow-800'); |
| statusBadge.textContent = 'Pendente'; |
| } |
| |
| statusCell.appendChild(statusBadge); |
| row.appendChild(dateCell); |
| row.appendChild(amountCell); |
| row.appendChild(statusCell); |
| |
| paymentHistoryTable.appendChild(row); |
| }); |
| |
| showModal('customer-details-modal'); |
| } |
| |
| function showConfirmationModal(title, message, action, data = null) { |
| document.getElementById('confirmation-title').textContent = title; |
| document.getElementById('confirmation-message').textContent = message; |
| currentAction = action; |
| currentActionData = data; |
| showModal('confirmation-modal'); |
| } |
| |
| function confirmAction() { |
| if (currentAction === 'deleteCustomer') { |
| deleteCustomer(currentActionData); |
| } else if (currentAction === 'deleteBill') { |
| deleteBill(currentActionData); |
| } |
| hideModal('confirmation-modal'); |
| } |
| |
| |
| function updateDashboard() { |
| |
| document.getElementById('active-customers').textContent = customers.length; |
| |
| |
| const now = new Date(); |
| const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); |
| const lastMonthStr = lastMonth.getFullYear() + '-' + String(lastMonth.getMonth() + 1).padStart(2, '0'); |
| |
| const lastMonthBills = bills.filter(bill => { |
| const billDate = new Date(bill.date); |
| const billMonth = billDate.getFullYear() + '-' + String(billDate.getMonth() + 1).padStart(2, '0'); |
| return billMonth === lastMonthStr && bill.status === 'paid'; |
| }); |
| |
| const monthlyRevenue = lastMonthBills.reduce((sum, bill) => sum + bill.amount + bill.lateFee, 0); |
| document.getElementById('monthly-revenue').textContent = `${monthlyRevenue} MZN`; |
| |
| |
| const overdueBills = bills.filter(bill => bill.status === 'overdue'); |
| document.getElementById('late-payments').textContent = overdueBills.length; |
| |
| |
| const consumptions = bills.map(bill => bill.consumption); |
| const avgConsumption = consumptions.length > 0 ? |
| (consumptions.reduce((sum, val) => sum + val, 0) / consumptions.length).toFixed(1) : 0; |
| document.getElementById('avg-consumption').textContent = `${avgConsumption} m³`; |
| } |
| |
| function loadRecentPayments() { |
| const recentPaymentsTable = document.getElementById('recent-payments'); |
| recentPaymentsTable.innerHTML = ''; |
| |
| |
| const paidBills = bills.filter(bill => bill.status === 'paid') |
| .sort((a, b) => new Date(b.paymentDate) - new Date(a.paymentDate)) |
| .slice(0, 5); |
| |
| paidBills.forEach(bill => { |
| const row = document.createElement('tr'); |
| |
| const customerCell = document.createElement('td'); |
| customerCell.className = 'px-6 py-4 whitespace-nowrap'; |
| customerCell.textContent = bill.customerName; |
| |
| const numberCell = document.createElement('td'); |
| numberCell.className = 'px-6 py-4 whitespace-nowrap'; |
| numberCell.textContent = bill.customerNumber; |
| |
| const areaCell = document.createElement('td'); |
| areaCell.className = 'px-6 py-4 whitespace-nowrap'; |
| areaCell.textContent = bill.areaName; |
| |
| const amountCell = document.createElement('td'); |
| amountCell.className = 'px-6 py-4 whitespace-nowrap'; |
| amountCell.textContent = `${bill.amount + bill.lateFee} MZN`; |
| |
| const dateCell = document.createElement('td'); |
| dateCell.className = 'px-6 py-4 whitespace-nowrap'; |
| dateCell.textContent = new Date(bill.paymentDate).toLocaleDateString('pt-PT'); |
| |
| const statusCell = document.createElement('td'); |
| statusCell.className = 'px-6 py-4 whitespace-nowrap'; |
| |
| const statusBadge = document.createElement('span'); |
| statusBadge.className = 'px-2 py-1 text-xs rounded-full bg-green-100 text-green-800'; |
| statusBadge.textContent = 'Pago'; |
| |
| statusCell.appendChild(statusBadge); |
| row.appendChild(customerCell); |
| row.appendChild(numberCell); |
| row.appendChild(areaCell); |
| row.appendChild(amountCell); |
| row.appendChild(dateCell); |
| row.appendChild(statusCell); |
| |
| recentPaymentsTable.appendChild(row); |
| }); |
| } |
| |
| function loadAlerts() { |
| const alertsContainer = document.getElementById('alerts-container'); |
| alertsContainer.innerHTML = ''; |
| |
| |
| const now = new Date(); |
| const twoMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 2, 1); |
| |
| const overdueBills = bills.filter(bill => { |
| if (bill.status !== 'overdue') return false; |
| |
| const dueDate = new Date(bill.dueDate); |
| return dueDate < twoMonthsAgo; |
| }); |
| |
| |
| const overdueCustomers = {}; |
| overdueBills.forEach(bill => { |
| if (!overdueCustomers[bill.customerId]) { |
| overdueCustomers[bill.customerId] = { |
| customer: customers.find(c => c.id === bill.customerId), |
| bills: [] |
| }; |
| } |
| overdueCustomers[bill.customerId].bills.push(bill); |
| }); |
| |
| |
| Object.values(overdueCustomers).forEach(customerData => { |
| const totalDebt = customerData.bills.reduce((sum, bill) => sum + bill.amount + bill.lateFee, 0); |
| const monthsOverdue = customerData.bills.length; |
| |
| const alert = document.createElement('div'); |
| alert.className = 'p-3 rounded bg-red-50 border border-red-200 flex justify-between items-center alert-pulse'; |
| |
| const alertContent = document.createElement('div'); |
| |
| const alertTitle = document.createElement('h4'); |
| alertTitle.className = 'font-semibold text-red-800'; |
| alertTitle.textContent = `${customerData.customer.name} (${customerData.customer.number})`; |
| |
| const alertMessage = document.createElement('p'); |
| alertMessage.className = 'text-sm text-red-600'; |
| alertMessage.textContent = `Dívida de ${totalDebt} MZN (${monthsOverdue} ${monthsOverdue === 1 ? 'mês' : 'meses'} atrasado)`; |
| |
| alertContent.appendChild(alertTitle); |
| alertContent.appendChild(alertMessage); |
| |
| const alertButton = document.createElement('button'); |
| alertButton.className = 'px-3 py-1 bg-red-100 hover:bg-red-200 text-red-800 rounded text-sm'; |
| alertButton.textContent = 'Ver Detalhes'; |
| alertButton.onclick = () => showCustomerDetails(customerData.customer.id); |
| |
| alert.appendChild(alertContent); |
| alert.appendChild(alertButton); |
| |
| alertsContainer.appendChild(alert); |
| }); |
| |
| |
| if (alertsContainer.children.length === 0) { |
| const noAlerts = document.createElement('div'); |
| noAlerts.className = 'p-3 rounded bg-blue-50 border border-blue-200 text-blue-800'; |
| noAlerts.textContent = 'Nenhum alerta de pagamento atrasado.'; |
| alertsContainer.appendChild(noAlerts); |
| } |
| } |
| |
| |
| function calculateAmount() { |
| const consumption = parseFloat(document.getElementById('consumption').value) || 5; |
| const minConsumption = settings.minConsumption; |
| const minPrice = settings.minConsumptionPrice; |
| const extraPrice = settings.extraConsumptionPrice; |
| |
| let amount = minPrice; |
| if (consumption > minConsumption) { |
| amount += (consumption - minConsumption) * extraPrice; |
| } |
| |
| document.getElementById('amount').value = amount.toFixed(2); |
| } |
| |
| function registerPayment() { |
| const customerSearch = document.getElementById('customer-search').value.trim(); |
| const consumption = parseFloat(document.getElementById('consumption').value) || 5; |
| const amount = parseFloat(document.getElementById('amount').value) || 300; |
| |
| if (!customerSearch) { |
| alert('Por favor, selecione um cliente.'); |
| return; |
| } |
| |
| |
| |
| |
| |
| const customer = customers.find(c => |
| c.name.toLowerCase().includes(customerSearch.toLowerCase()) || |
| c.number.toLowerCase().includes(customerSearch.toLowerCase()) |
| ); |
| |
| if (!customer) { |
| alert('Cliente não encontrado.'); |
| return; |
| } |
| |
| |
| document.getElementById('receipt-number').textContent = `REC-${Date.now().toString().slice(-6)}`; |
| document.getElementById('receipt-date').textContent = new Date().toLocaleDateString('pt-PT'); |
| document.getElementById('receipt-customer').textContent = `${customer.number} - ${customer.name}`; |
| document.getElementById('receipt-water-amount').textContent = `${amount} MZN`; |
| document.getElementById('receipt-late-fee').textContent = '0 MZN'; |
| document.getElementById('receipt-total').textContent = `${amount} MZN`; |
| |
| showModal('payment-receipt-modal'); |
| |
| |
| document.getElementById('customer-search').value = ''; |
| document.getElementById('consumption').value = '5'; |
| document.getElementById('amount').value = '300'; |
| |
| |
| const now = new Date(); |
| const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1); |
| const dueDate = new Date(now.getFullYear(), now.getMonth(), settings.dueDate); |
| |
| const newBill = { |
| id: bills.length + 1, |
| customerId: customer.id, |
| customerNumber: customer.number, |
| customerName: customer.name, |
| area: customer.area, |
| areaName: customer.areaName, |
| consumption: consumption, |
| amount: amount, |
| date: currentMonth.toISOString().split('T')[0], |
| dueDate: dueDate.toISOString().split('T')[0], |
| paymentDate: now.toISOString().split('T')[0], |
| status: 'paid', |
| lateFee: 0 |
| }; |
| |
| bills.push(newBill); |
| |
| |
| updateDashboard(); |
| loadRecentPayments(); |
| loadAlerts(); |
| loadBillingHistory(); |
| } |
| |
| function searchCustomers(query) { |
| const resultsContainer = document.getElementById('search-results'); |
| resultsContainer.innerHTML = ''; |
| |
| if (query.length < 2) { |
| resultsContainer.classList.add('hidden'); |
| return; |
| } |
| |
| const filteredCustomers = customers.filter(customer => |
| customer.name.toLowerCase().includes(query.toLowerCase()) || |
| customer.number.toLowerCase().includes(query.toLowerCase()) |
| ); |
| |
| if (filteredCustomers.length === 0) { |
| const noResults = document.createElement('div'); |
| noResults.className = 'p-2 text-sm text-gray-500'; |
| noResults.textContent = 'Nenhum cliente encontrado'; |
| resultsContainer.appendChild(noResults); |
| } else { |
| filteredCustomers.forEach(customer => { |
| const resultItem = document.createElement('div'); |
| resultItem.className = 'p-2 hover:bg-gray-100 cursor-pointer'; |
| resultItem.textContent = `${customer.number} - ${customer.name}`; |
| resultItem.onclick = () => { |
| document.getElementById('customer-search').value = `${customer.number} - ${customer.name}`; |
| resultsContainer.classList.add('hidden'); |
| }; |
| resultsContainer.appendChild(resultItem); |
| }); |
| } |
| |
| resultsContainer.classList.remove('hidden'); |
| } |
| |
| function loadBillingHistory() { |
| const billingHistoryTable = document.getElementById('billing-history'); |
| billingHistoryTable.innerHTML = ''; |
| |
| const filter = document.getElementById('billing-filter').value; |
| const monthFilter = document.getElementById('billing-month').value; |
| |
| let filteredBills = [...bills]; |
| |
| |
| if (filter === 'paid') { |
| filteredBills = filteredBills.filter(bill => bill.status === 'paid'); |
| } else if (filter === 'pending') { |
| filteredBills = filteredBills.filter(bill => bill.status === 'pending'); |
| } else if (filter === 'overdue') { |
| filteredBills = filteredBills.filter(bill => bill.status === 'overdue'); |
| } |
| |
| |
| if (monthFilter) { |
| filteredBills = filteredBills.filter(bill => { |
| const billDate = new Date(bill.date); |
| const billMonth = billDate.getFullYear() + '-' + String(billDate.getMonth() + 1).padStart(2, '0'); |
| return billMonth === monthFilter; |
| }); |
| } |
| |
| |
| filteredBills.sort((a, b) => new Date(b.date) - new Date(a.date)); |
| |
| filteredBills.forEach(bill => { |
| const row = document.createElement('tr'); |
| |
| const numberCell = document.createElement('td'); |
| numberCell.className = 'px-6 py-4 whitespace-nowrap'; |
| numberCell.textContent = bill.customerNumber; |
| |
| const customerCell = document.createElement('td'); |
| customerCell.className = 'px-6 py-4 whitespace-nowrap'; |
| customerCell.textContent = bill.customerName; |
| |
| const areaCell = document.createElement('td'); |
| areaCell.className = 'px-6 py-4 whitespace-nowrap'; |
| areaCell.textContent = bill.areaName; |
| |
| const consumptionCell = document.createElement('td'); |
| consumptionCell.className = 'px-6 py-4 whitespace-nowrap'; |
| consumptionCell.textContent = `${bill.consumption} m³`; |
| |
| const amountCell = document.createElement('td'); |
| amountCell.className = 'px-6 py-4 whitespace-nowrap'; |
| amountCell.textContent = `${bill.amount + bill.lateFee} MZN`; |
| |
| const dateCell = document.createElement('td'); |
| dateCell.className = 'px-6 py-4 whitespace-nowrap'; |
| dateCell.textContent = new Date(bill.date).toLocaleDateString('pt-PT'); |
| |
| const statusCell = document.createElement('td'); |
| statusCell.className = 'px-6 py-4 whitespace-nowrap'; |
| |
| const statusBadge = document.createElement('span'); |
| statusBadge.className = 'px-2 py-1 text-xs rounded-full'; |
| |
| if (bill.status === 'paid') { |
| statusBadge.classList.add('bg-green-100', 'text-green-800'); |
| statusBadge.textContent = 'Pago'; |
| } else if (bill.status === 'overdue') { |
| statusBadge.classList.add('bg-red-100', 'text-red-800'); |
| statusBadge.textContent = 'Atrasado'; |
| } else { |
| statusBadge.classList.add('bg-yellow-100', 'text-yellow-800'); |
| statusBadge.textContent = 'Pendente'; |
| } |
| |
| statusCell.appendChild(statusBadge); |
| |
| const actionsCell = document.createElement('td'); |
| actionsCell.className = 'px-6 py-4 whitespace-nowrap text-right text-sm font-medium'; |
| |
| const viewButton = document.createElement('button'); |
| viewButton.className = 'text-primary hover:text-secondary mr-2'; |
| viewButton.innerHTML = '<i class="fas fa-eye"></i>'; |
| viewButton.title = 'Ver Detalhes'; |
| viewButton.onclick = () => viewBillDetails(bill.id); |
| |
| const editButton = document.createElement('button'); |
| editButton.className = 'text-yellow-600 hover:text-yellow-800 mr-2'; |
| editButton.innerHTML = '<i class="fas fa-edit"></i>'; |
| editButton.title = 'Editar'; |
| editButton.onclick = () => editBill(bill.id); |
| |
| const deleteButton = document.createElement('button'); |
| deleteButton.className = 'text-red-600 hover:text-red-800'; |
| deleteButton.innerHTML = '<i class="fas fa-trash"></i>'; |
| deleteButton.title = 'Eliminar'; |
| deleteButton.onclick = () => confirmDeleteBill(bill.id); |
| |
| actionsCell.appendChild(viewButton); |
| actionsCell.appendChild(editButton); |
| actionsCell.appendChild(deleteButton); |
| |
| row.appendChild(numberCell); |
| row.appendChild(customerCell); |
| row.appendChild(areaCell); |
| row.appendChild(consumptionCell); |
| row.appendChild(amountCell); |
| row.appendChild(dateCell); |
| row.appendChild(statusCell); |
| row.appendChild(actionsCell); |
| |
| billingHistoryTable.appendChild(row); |
| }); |
| |
| |
| if (filteredBills.length === 0) { |
| const emptyRow = document.createElement('tr'); |
| const emptyCell = document.createElement('td'); |
| emptyCell.colSpan = 8; |
| emptyCell.className = 'px-6 py-4 text-center text-gray-500'; |
| emptyCell.textContent = 'Nenhuma cobrança encontrada'; |
| emptyRow.appendChild(emptyCell); |
| billingHistoryTable.appendChild(emptyRow); |
| } |
| } |
| |
| function viewBillDetails(billId) { |
| const bill = bills.find(b => b.id == billId); |
| if (!bill) return; |
| |
| |
| document.getElementById('receipt-number').textContent = `REC-${bill.id.toString().padStart(6, '0')}`; |
| document.getElementById('receipt-date').textContent = bill.paymentDate ? |
| new Date(bill.paymentDate).toLocaleDateString('pt-PT') : |
| new Date().toLocaleDateString('pt-PT'); |
| |
| const customer = customers.find(c => c.id == bill.customerId); |
| document.getElementById('receipt-customer').textContent = customer ? |
| `${customer.number} - ${customer |
| </html> |