| | <!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> |