| | <!DOCTYPE html> |
| | <html lang="pt-BR"> |
| | <head> |
| | <meta charset="UTF-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| | <title>PharmaManager - Sistema de Gerenciamento de Farmácia</title> |
| | <script src="https://cdn.tailwindcss.com"></script> |
| | <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| | <style> |
| | .sidebar { |
| | transition: all 0.3s ease; |
| | } |
| | .sidebar.collapsed { |
| | width: 70px; |
| | } |
| | .sidebar.collapsed .sidebar-text { |
| | display: none; |
| | } |
| | .sidebar.collapsed .logo-text { |
| | display: none; |
| | } |
| | .main-content { |
| | transition: all 0.3s ease; |
| | } |
| | .main-content.expanded { |
| | margin-left: 70px; |
| | } |
| | @media (max-width: 768px) { |
| | .sidebar { |
| | width: 0; |
| | overflow: hidden; |
| | } |
| | .sidebar.collapsed { |
| | width: 0; |
| | } |
| | .main-content { |
| | margin-left: 0; |
| | } |
| | .mobile-menu-btn { |
| | display: block; |
| | } |
| | } |
| | .toast { |
| | animation: fadeInOut 3s ease-in-out; |
| | } |
| | @keyframes fadeInOut { |
| | 0% { opacity: 0; } |
| | 10% { opacity: 1; } |
| | 90% { opacity: 1; } |
| | 100% { opacity: 0; } |
| | } |
| | </style> |
| | </head> |
| | <body class="bg-gray-100"> |
| | |
| | <button class="mobile-menu-btn fixed top-4 left-4 z-50 bg-blue-600 text-white p-2 rounded-md shadow-lg md:hidden"> |
| | <i class="fas fa-bars"></i> |
| | </button> |
| |
|
| | |
| | <div class="sidebar fixed h-full bg-blue-800 text-white w-64 shadow-lg"> |
| | <div class="p-4 flex items-center"> |
| | <i class="fas fa-pills text-2xl mr-2"></i> |
| | <span class="logo-text text-xl font-bold">PharmaManager</span> |
| | </div> |
| | <nav class="mt-6"> |
| | <div class="px-4 py-2 hover:bg-blue-700 cursor-pointer flex items-center"> |
| | <i class="fas fa-tachometer-alt mr-3"></i> |
| | <span class="sidebar-text">Dashboard</span> |
| | </div> |
| | <div class="px-4 py-2 hover:bg-blue-700 cursor-pointer flex items-center" id="customer-nav"> |
| | <i class="fas fa-users mr-3"></i> |
| | <span class="sidebar-text">Clientes</span> |
| | </div> |
| | <div class="px-4 py-2 hover:bg-blue-700 cursor-pointer flex items-center" id="products-nav"> |
| | <i class="fas fa-boxes mr-3"></i> |
| | <span class="sidebar-text">Produtos</span> |
| | </div> |
| | <div class="px-4 py-2 hover:bg-blue-700 cursor-pointer flex items-center" id="sales-nav"> |
| | <i class="fas fa-shopping-cart mr-3"></i> |
| | <span class="sidebar-text">Vendas</span> |
| | </div> |
| | <div class="px-4 py-2 hover:bg-blue-700 cursor-pointer flex items-center" id="inventory-nav"> |
| | <i class="fas fa-warehouse mr-3"></i> |
| | <span class="sidebar-text">Estoque</span> |
| | </div> |
| | <div class="px-4 py-2 hover:bg-blue-700 cursor-pointer flex items-center" id="reports-nav"> |
| | <i class="fas fa-chart-bar mr-3"></i> |
| | <span class="sidebar-text">Relatórios</span> |
| | </div> |
| | <div class="px-4 py-2 hover:bg-blue-700 cursor-pointer flex items-center" id="settings-nav"> |
| | <i class="fas fa-cog mr-3"></i> |
| | <span class="sidebar-text">Configurações</span> |
| | </div> |
| | <div class="px-4 py-2 hover:bg-blue-700 cursor-pointer flex items-center" id="backup-nav"> |
| | <i class="fas fa-database mr-3"></i> |
| | <span class="sidebar-text">Backup/Restore</span> |
| | </div> |
| | </nav> |
| | </div> |
| |
|
| | |
| | <div class="main-content ml-64 min-h-screen"> |
| | |
| | <header class="bg-white shadow-sm p-4 flex justify-between items-center"> |
| | <h1 class="text-2xl font-semibold text-gray-800" id="page-title">Dashboard</h1> |
| | <div class="flex items-center space-x-4"> |
| | <div class="relative"> |
| | <i class="fas fa-bell text-gray-600 cursor-pointer hover:text-blue-600"></i> |
| | <span class="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">3</span> |
| | </div> |
| | <div class="flex items-center space-x-2"> |
| | <img src="https://via.placeholder.com/40" alt="User" class="rounded-full"> |
| | <span class="text-gray-700">Admin</span> |
| | </div> |
| | </div> |
| | </header> |
| |
|
| | |
| | <div class="p-6"> |
| | |
| | <div id="dashboard-content"> |
| | <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6"> |
| | <div class="bg-white p-6 rounded-lg shadow-md"> |
| | <div class="flex justify-between items-center"> |
| | <div> |
| | <p class="text-gray-500">Clientes Cadastrados</p> |
| | <h3 class="text-2xl font-bold" id="customer-count">0</h3> |
| | </div> |
| | <div class="bg-blue-100 p-3 rounded-full"> |
| | <i class="fas fa-users text-blue-600"></i> |
| | </div> |
| | </div> |
| | </div> |
| | <div class="bg-white p-6 rounded-lg shadow-md"> |
| | <div class="flex justify-between items-center"> |
| | <div> |
| | <p class="text-gray-500">Produtos em Estoque</p> |
| | <h3 class="text-2xl font-bold" id="product-count">0</h3> |
| | </div> |
| | <div class="bg-green-100 p-3 rounded-full"> |
| | <i class="fas fa-boxes text-green-600"></i> |
| | </div> |
| | </div> |
| | </div> |
| | <div class="bg-white p-6 rounded-lg shadow-md"> |
| | <div class="flex justify-between items-center"> |
| | <div> |
| | <p class="text-gray-500">Vendas Hoje</p> |
| | <h3 class="text-2xl font-bold" id="sales-today">0</h3> |
| | </div> |
| | <div class="bg-yellow-100 p-3 rounded-full"> |
| | <i class="fas fa-shopping-cart text-yellow-600"></i> |
| | </div> |
| | </div> |
| | </div> |
| | <div class="bg-white p-6 rounded-lg shadow-md"> |
| | <div class="flex justify-between items-center"> |
| | <div> |
| | <p class="text-gray-500">Receita Mensal</p> |
| | <h3 class="text-2xl font-bold" id="monthly-revenue">R$ 0,00</h3> |
| | </div> |
| | <div class="bg-purple-100 p-3 rounded-full"> |
| | <i class="fas fa-money-bill-wave text-purple-600"></i> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <div class="grid grid-cols-1 lg:grid-cols-3 gap-6"> |
| | <div class="bg-white p-6 rounded-lg shadow-md lg:col-span-2"> |
| | <h2 class="text-xl font-semibold mb-4">Vendas Recentes</h2> |
| | <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">ID</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">Data</th> |
| | <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Total</th> |
| | </tr> |
| | </thead> |
| | <tbody class="bg-white divide-y divide-gray-200" id="recent-sales"> |
| | <tr> |
| | <td colspan="4" class="px-6 py-4 text-center text-gray-500">Nenhuma venda recente</td> |
| | </tr> |
| | </tbody> |
| | </table> |
| | </div> |
| | </div> |
| | <div class="bg-white p-6 rounded-lg shadow-md"> |
| | <h2 class="text-xl font-semibold mb-4">Estoque Baixo</h2> |
| | <div class="space-y-4" id="low-stock"> |
| | <div class="text-center text-gray-500">Nenhum produto com estoque baixo</div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div id="customer-content" class="hidden"> |
| | <div class="flex justify-between items-center mb-6"> |
| | <h2 class="text-2xl font-semibold">Gerenciamento de Clientes</h2> |
| | <button id="add-customer-btn" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 flex items-center"> |
| | <i class="fas fa-plus mr-2"></i> Novo Cliente |
| | </button> |
| | </div> |
| |
|
| | <div class="bg-white rounded-lg shadow-md overflow-hidden"> |
| | <div class="p-4 border-b flex justify-between items-center"> |
| | <div class="relative w-64"> |
| | <input type="text" id="customer-search" placeholder="Buscar clientes..." class="w-full pl-10 pr-4 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
| | <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
| | </div> |
| | <div class="flex space-x-2"> |
| | <button id="export-customers" class="bg-green-600 text-white px-3 py-1 rounded-md hover:bg-green-700 flex items-center"> |
| | <i class="fas fa-file-export mr-1"></i> Exportar |
| | </button> |
| | <button id="import-customers" class="bg-purple-600 text-white px-3 py-1 rounded-md hover:bg-purple-700 flex items-center"> |
| | <i class="fas fa-file-import mr-1"></i> Importar |
| | </button> |
| | </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">ID</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">CPF</th> |
| | <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Telefone</th> |
| | <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</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 class="bg-white divide-y divide-gray-200" id="customer-table-body"> |
| | |
| | </tbody> |
| | </table> |
| | </div> |
| | <div class="p-4 border-t flex justify-between items-center"> |
| | <div class="text-sm text-gray-500"> |
| | Mostrando <span id="customer-start">1</span> a <span id="customer-end">10</span> de <span id="customer-total">0</span> clientes |
| | </div> |
| | <div class="flex space-x-2"> |
| | <button id="customer-prev" class="px-3 py-1 border rounded-md disabled:opacity-50" disabled>Anterior</button> |
| | <button id="customer-next" class="px-3 py-1 border rounded-md disabled:opacity-50" disabled>Próximo</button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div id="backup-content" class="hidden"> |
| | <div class="flex justify-between items-center mb-6"> |
| | <h2 class="text-2xl font-semibold">Backup e Restauração</h2> |
| | </div> |
| |
|
| | <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| | <div class="bg-white p-6 rounded-lg shadow-md"> |
| | <h3 class="text-xl font-semibold mb-4 flex items-center"> |
| | <i class="fas fa-download mr-2 text-blue-600"></i> Criar Backup |
| | </h3> |
| | <p class="text-gray-600 mb-4">Crie um backup completo do banco de dados para restaurar posteriormente.</p> |
| | <button id="create-backup-btn" class="bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 flex items-center w-full justify-center"> |
| | <i class="fas fa-save mr-2"></i> Criar Backup |
| | </button> |
| | </div> |
| |
|
| | <div class="bg-white p-6 rounded-lg shadow-md"> |
| | <h3 class="text-xl font-semibold mb-4 flex items-center"> |
| | <i class="fas fa-upload mr-2 text-green-600"></i> Restaurar Backup |
| | </h3> |
| | <p class="text-gray-600 mb-4">Restaurar o banco de dados a partir de um arquivo de backup.</p> |
| | <div class="mb-4"> |
| | <label class="block text-gray-700 mb-2">Selecione o arquivo de backup:</label> |
| | <input type="file" id="restore-file" class="block w-full text-sm text-gray-500 |
| | file:mr-4 file:py-2 file:px-4 |
| | file:rounded-md file:border-0 |
| | file:text-sm file:font-semibold |
| | file:bg-blue-50 file:text-blue-700 |
| | hover:file:bg-blue-100"> |
| | </div> |
| | <button id="restore-backup-btn" class="bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 flex items-center w-full justify-center"> |
| | <i class="fas fa-redo mr-2"></i> Restaurar Backup |
| | </button> |
| | </div> |
| | </div> |
| |
|
| | <div class="bg-white p-6 rounded-lg shadow-md mt-6"> |
| | <h3 class="text-xl font-semibold mb-4 flex items-center"> |
| | <i class="fas fa-history mr-2 text-purple-600"></i> Backups Disponíveis |
| | </h3> |
| | <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">Data</th> |
| | <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tamanho</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 class="bg-white divide-y divide-gray-200" id="backup-list"> |
| | |
| | </tbody> |
| | </table> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div id="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-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto"> |
| | <div class="p-6"> |
| | <div class="flex justify-between items-center mb-4"> |
| | <h3 class="text-xl font-semibold" id="modal-title">Adicionar Novo Cliente</h3> |
| | <button id="close-modal" class="text-gray-500 hover:text-gray-700"> |
| | <i class="fas fa-times"></i> |
| | </button> |
| | </div> |
| | <form id="customer-form"> |
| | <input type="hidden" id="customer-id"> |
| | <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> |
| | <div> |
| | <label for="customer-name" class="block text-gray-700 mb-2">Nome Completo*</label> |
| | <input type="text" id="customer-name" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required> |
| | </div> |
| | <div> |
| | <label for="customer-cpf" class="block text-gray-700 mb-2">CPF*</label> |
| | <input type="text" id="customer-cpf" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required> |
| | </div> |
| | </div> |
| | <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> |
| | <div> |
| | <label for="customer-phone" class="block text-gray-700 mb-2">Telefone*</label> |
| | <input type="text" id="customer-phone" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" required> |
| | </div> |
| | <div> |
| | <label for="customer-email" class="block text-gray-700 mb-2">Email</label> |
| | <input type="email" id="customer-email" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
| | </div> |
| | </div> |
| | <div class="mb-4"> |
| | <label for="customer-address" class="block text-gray-700 mb-2">Endereço</label> |
| | <input type="text" id="customer-address" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
| | </div> |
| | <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6"> |
| | <div> |
| | <label for="customer-city" class="block text-gray-700 mb-2">Cidade</label> |
| | <input type="text" id="customer-city" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
| | </div> |
| | <div> |
| | <label for="customer-state" class="block text-gray-700 mb-2">Estado</label> |
| | <input type="text" id="customer-state" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
| | </div> |
| | <div> |
| | <label for="customer-zip" class="block text-gray-700 mb-2">CEP</label> |
| | <input type="text" id="customer-zip" class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"> |
| | </div> |
| | </div> |
| | <div class="flex justify-end space-x-3"> |
| | <button type="button" id="cancel-customer" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-100">Cancelar</button> |
| | <button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Salvar</button> |
| | </div> |
| | </form> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div id="toast" class="fixed bottom-4 right-4 bg-green-500 text-white px-6 py-3 rounded-md shadow-lg hidden"> |
| | <div class="flex items-center"> |
| | <i class="fas fa-check-circle mr-2"></i> |
| | <span id="toast-message">Operação realizada com sucesso!</span> |
| | </div> |
| | </div> |
| |
|
| | |
| | <div id="confirm-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-xl w-full max-w-md"> |
| | <div class="p-6"> |
| | <div class="flex justify-between items-center mb-4"> |
| | <h3 class="text-xl font-semibold" id="confirm-title">Confirmar ação</h3> |
| | <button id="close-confirm" class="text-gray-500 hover:text-gray-700"> |
| | <i class="fas fa-times"></i> |
| | </button> |
| | </div> |
| | <p id="confirm-message">Tem certeza que deseja realizar esta ação?</p> |
| | <div class="flex justify-end space-x-3 mt-6"> |
| | <button type="button" id="cancel-confirm" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-100">Cancelar</button> |
| | <button type="button" id="confirm-action" class="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700">Confirmar</button> |
| | </div> |
| | </div> |
| | </div> |
| | </div> |
| |
|
| | <script> |
| | |
| | let db; |
| | const DB_NAME = "PharmaManagerDB"; |
| | const DB_VERSION = 1; |
| | |
| | |
| | const request = indexedDB.open(DB_NAME, DB_VERSION); |
| | |
| | request.onupgradeneeded = (event) => { |
| | db = event.target.result; |
| | |
| | |
| | if (!db.objectStoreNames.contains('customers')) { |
| | const customersStore = db.createObjectStore('customers', { keyPath: 'id', autoIncrement: true }); |
| | customersStore.createIndex('name', 'name', { unique: false }); |
| | customersStore.createIndex('cpf', 'cpf', { unique: true }); |
| | customersStore.createIndex('phone', 'phone', { unique: false }); |
| | } |
| | |
| | |
| | if (!db.objectStoreNames.contains('products')) { |
| | const productsStore = db.createObjectStore('products', { keyPath: 'id', autoIncrement: true }); |
| | productsStore.createIndex('name', 'name', { unique: false }); |
| | productsStore.createIndex('barcode', 'barcode', { unique: true }); |
| | } |
| | |
| | |
| | if (!db.objectStoreNames.contains('sales')) { |
| | const salesStore = db.createObjectStore('sales', { keyPath: 'id', autoIncrement: true }); |
| | salesStore.createIndex('customerId', 'customerId', { unique: false }); |
| | salesStore.createIndex('date', 'date', { unique: false }); |
| | } |
| | |
| | |
| | if (!db.objectStoreNames.contains('backups')) { |
| | db.createObjectStore('backups', { keyPath: 'id', autoIncrement: true }); |
| | } |
| | }; |
| | |
| | request.onsuccess = (event) => { |
| | db = event.target.result; |
| | console.log("Database opened successfully"); |
| | updateDashboardCounts(); |
| | loadCustomers(); |
| | loadBackups(); |
| | }; |
| | |
| | request.onerror = (event) => { |
| | console.error("Database error:", event.target.error); |
| | }; |
| | |
| | |
| | document.addEventListener('DOMContentLoaded', () => { |
| | |
| | const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); |
| | const sidebar = document.querySelector('.sidebar'); |
| | const mainContent = document.querySelector('.main-content'); |
| | |
| | mobileMenuBtn.addEventListener('click', () => { |
| | sidebar.classList.toggle('hidden'); |
| | }); |
| | |
| | |
| | document.getElementById('customer-nav').addEventListener('click', () => { |
| | showSection('customer-content', 'Clientes'); |
| | loadCustomers(); |
| | }); |
| | |
| | document.getElementById('backup-nav').addEventListener('click', () => { |
| | showSection('backup-content', 'Backup/Restore'); |
| | loadBackups(); |
| | }); |
| | |
| | |
| | document.getElementById('add-customer-btn').addEventListener('click', () => { |
| | openCustomerModal(); |
| | }); |
| | |
| | document.getElementById('close-modal').addEventListener('click', () => { |
| | closeCustomerModal(); |
| | }); |
| | |
| | document.getElementById('cancel-customer').addEventListener('click', () => { |
| | closeCustomerModal(); |
| | }); |
| | |
| | document.getElementById('customer-form').addEventListener('submit', (e) => { |
| | e.preventDefault(); |
| | saveCustomer(); |
| | }); |
| | |
| | |
| | document.getElementById('create-backup-btn').addEventListener('click', () => { |
| | createBackup(); |
| | }); |
| | |
| | document.getElementById('restore-backup-btn').addEventListener('click', () => { |
| | restoreBackup(); |
| | }); |
| | |
| | |
| | showSection('dashboard-content', 'Dashboard'); |
| | }); |
| | |
| | function showSection(sectionId, title) { |
| | |
| | document.querySelectorAll('[id$="-content"]').forEach(section => { |
| | section.classList.add('hidden'); |
| | }); |
| | |
| | |
| | document.getElementById(sectionId).classList.remove('hidden'); |
| | |
| | |
| | document.getElementById('page-title').textContent = title; |
| | } |
| | |
| | |
| | function loadCustomers(page = 1, search = '') { |
| | const transaction = db.transaction(['customers'], 'readonly'); |
| | const store = transaction.objectStore('customers'); |
| | const request = store.getAll(); |
| | |
| | request.onsuccess = () => { |
| | const customers = request.result; |
| | let filteredCustomers = customers; |
| | |
| | |
| | if (search) { |
| | const searchTerm = search.toLowerCase(); |
| | filteredCustomers = customers.filter(customer => |
| | customer.name.toLowerCase().includes(searchTerm) || |
| | customer.cpf.includes(searchTerm) || |
| | customer.phone.includes(searchTerm) || |
| | (customer.email && customer.email.toLowerCase().includes(searchTerm)) |
| | ); |
| | } |
| | |
| | |
| | const itemsPerPage = 10; |
| | const startIndex = (page - 1) * itemsPerPage; |
| | const endIndex = startIndex + itemsPerPage; |
| | const paginatedCustomers = filteredCustomers.slice(startIndex, endIndex); |
| | const totalPages = Math.ceil(filteredCustomers.length / itemsPerPage); |
| | |
| | |
| | const tbody = document.getElementById('customer-table-body'); |
| | tbody.innerHTML = ''; |
| | |
| | if (paginatedCustomers.length === 0) { |
| | const row = document.createElement('tr'); |
| | row.innerHTML = `<td colspan="6" class="px-6 py-4 text-center text-gray-500">Nenhum cliente encontrado</td>`; |
| | tbody.appendChild(row); |
| | } else { |
| | paginatedCustomers.forEach(customer => { |
| | const row = document.createElement('tr'); |
| | row.innerHTML = ` |
| | <td class="px-6 py-4 whitespace-nowrap">${customer.id}</td> |
| | <td class="px-6 py-4 whitespace-nowrap">${customer.name}</td> |
| | <td class="px-6 py-4 whitespace-nowrap">${formatCPF(customer.cpf)}</td> |
| | <td class="px-6 py-4 whitespace-nowrap">${formatPhone(customer.phone)}</td> |
| | <td class="px-6 py-4 whitespace-nowrap">${customer.email || '-'}</td> |
| | <td class="px-6 py-4 whitespace-nowrap"> |
| | <button class="edit-customer text-blue-600 hover:text-blue-800 mr-3" data-id="${customer.id}"> |
| | <i class="fas fa-edit"></i> |
| | </button> |
| | <button class="delete-customer text-red-600 hover:text-red-800" data-id="${customer.id}"> |
| | <i class="fas fa-trash"></i> |
| | </button> |
| | </td> |
| | `; |
| | tbody.appendChild(row); |
| | }); |
| | |
| | |
| | document.querySelectorAll('.edit-customer').forEach(btn => { |
| | btn.addEventListener('click', (e) => { |
| | const id = parseInt(e.currentTarget.getAttribute('data-id')); |
| | editCustomer(id); |
| | }); |
| | }); |
| | |
| | document.querySelectorAll('.delete-customer').forEach(btn => { |
| | btn.addEventListener('click', (e) => { |
| | const id = parseInt(e.currentTarget.getAttribute('data-id')); |
| | confirmDeleteCustomer(id); |
| | }); |
| | }); |
| | } |
| | |
| | |
| | document.getElementById('customer-start').textContent = startIndex + 1; |
| | document.getElementById('customer-end').textContent = Math.min(endIndex, filteredCustomers.length); |
| | document.getElementById('customer-total').textContent = filteredCustomers.length; |
| | |
| | |
| | const prevBtn = document.getElementById('customer-prev'); |
| | const nextBtn = document.getElementById('customer-next'); |
| | |
| | prevBtn.disabled = page <= 1; |
| | nextBtn.disabled = page >= totalPages; |
| | |
| | prevBtn.onclick = () => loadCustomers(page - 1, search); |
| | nextBtn.onclick = () => loadCustomers(page + 1, search); |
| | }; |
| | |
| | request.onerror = () => { |
| | console.error("Error loading customers"); |
| | }; |
| | } |
| | |
| | function openCustomerModal(customer = null) { |
| | const modal = document.getElementById('customer-modal'); |
| | const form = document.getElementById('customer-form'); |
| | |
| | if (customer) { |
| | document.getElementById('modal-title').textContent = 'Editar Cliente'; |
| | document.getElementById('customer-id').value = customer.id; |
| | document.getElementById('customer-name').value = customer.name; |
| | document.getElementById('customer-cpf').value = customer.cpf; |
| | document.getElementById('customer-phone').value = customer.phone; |
| | document.getElementById('customer-email').value = customer.email || ''; |
| | document.getElementById('customer-address').value = customer.address || ''; |
| | document.getElementById('customer-city').value = customer.city || ''; |
| | document.getElementById('customer-state').value = customer.state || ''; |
| | document.getElementById('customer-zip').value = customer.zip || ''; |
| | } else { |
| | document.getElementById('modal-title').textContent = 'Adicionar Novo Cliente'; |
| | form.reset(); |
| | } |
| | |
| | modal.classList.remove('hidden'); |
| | } |
| | |
| | function closeCustomerModal() { |
| | document.getElementById('customer-modal').classList.add('hidden'); |
| | } |
| | |
| | function saveCustomer() { |
| | const id = document.getElementById('customer-id').value; |
| | const customer = { |
| | name: document.getElementById('customer-name').value.trim(), |
| | cpf: document.getElementById('customer-cpf').value.replace(/\D/g, ''), |
| | phone: document.getElementById('customer-phone').value.replace(/\D/g, ''), |
| | email: document.getElementById('customer-email').value.trim() || null, |
| | address: document.getElementById('customer-address').value.trim() || null, |
| | city: document.getElementById('customer-city').value.trim() || null, |
| | state: document.getElementById('customer-state').value.trim() || null, |
| | zip: document.getElementById('customer-zip').value.replace(/\D/g, '') || null |
| | }; |
| | |
| | if (!customer.name || !customer.cpf || !customer.phone) { |
| | showToast('Por favor, preencha os campos obrigatórios', 'error'); |
| | return; |
| | } |
| | |
| | const transaction = db.transaction(['customers'], 'readwrite'); |
| | const store = transaction.objectStore('customers'); |
| | |
| | if (id) { |
| | customer.id = parseInt(id); |
| | const request = store.put(customer); |
| | |
| | request.onsuccess = () => { |
| | showToast('Cliente atualizado com sucesso!'); |
| | closeCustomerModal(); |
| | loadCustomers(); |
| | updateDashboardCounts(); |
| | }; |
| | |
| | request.onerror = () => { |
| | showToast('Erro ao atualizar cliente', 'error'); |
| | }; |
| | } else { |
| | const request = store.add(customer); |
| | |
| | request.onsuccess = () => { |
| | showToast('Cliente cadastrado com sucesso!'); |
| | closeCustomerModal(); |
| | loadCustomers(); |
| | updateDashboardCounts(); |
| | }; |
| | |
| | request.onerror = (e) => { |
| | if (e.target.error.name === 'ConstraintError') { |
| | showToast('CPF já cadastrado', 'error'); |
| | } else { |
| | showToast('Erro ao cadastrar cliente', 'error'); |
| | } |
| | }; |
| | } |
| | } |
| | |
| | function editCustomer(id) { |
| | const transaction = db.transaction(['customers'], 'readonly'); |
| | const store = transaction.objectStore('customers'); |
| | const request = store.get(id); |
| | |
| | request.onsuccess = () => { |
| | openCustomerModal(request.result); |
| | }; |
| | |
| | request.onerror = () => { |
| | showToast('Erro ao carregar cliente', 'error'); |
| | }; |
| | } |
| | |
| | function confirmDeleteCustomer(id) { |
| | const modal = document.getElementById('confirm-modal'); |
| | document.getElementById('confirm-title').textContent = 'Excluir Cliente'; |
| | document.getElementById('confirm-message').textContent = 'Tem certeza que deseja excluir este cliente? Esta ação não pode ser desfeita.'; |
| | |
| | document.getElementById('confirm-action').onclick = () => { |
| | deleteCustomer(id); |
| | modal.classList.add('hidden'); |
| | }; |
| | |
| | modal.classList.remove('hidden'); |
| | } |
| | |
| | function deleteCustomer(id) { |
| | const transaction = db.transaction(['customers'], 'readwrite'); |
| | const store = transaction.objectStore('customers'); |
| | const request = store.delete(id); |
| | |
| | request.onsuccess = () => { |
| | showToast('Cliente excluído com sucesso!'); |
| | loadCustomers(); |
| | updateDashboardCounts(); |
| | }; |
| | |
| | request.onerror = () => { |
| | showToast('Erro ao excluir cliente', 'error'); |
| | }; |
| | } |
| | |
| | |
| | function createBackup() { |
| | |
| | const transaction = db.transaction(['customers', 'products', 'sales'], 'readonly'); |
| | |
| | const customersRequest = transaction.objectStore('customers').getAll(); |
| | const productsRequest = transaction.objectStore('products').getAll(); |
| | const salesRequest = transaction.objectStore('sales').getAll(); |
| | |
| | Promise.all([ |
| | new Promise(resolve => { customersRequest.onsuccess = () => resolve(customersRequest.result); }), |
| | new Promise(resolve => { productsRequest.onsuccess = () => resolve(productsRequest.result); }), |
| | new Promise(resolve => { salesRequest.onsuccess = () => resolve(salesRequest.result); }) |
| | ]).then(([customers, products, sales]) => { |
| | const backupData = { |
| | timestamp: new Date().getTime(), |
| | date: new Date().toISOString(), |
| | customers, |
| | products, |
| | sales |
| | }; |
| | |
| | |
| | const backupTransaction = db.transaction(['backups'], 'readwrite'); |
| | const backupStore = backupTransaction.objectStore('backups'); |
| | backupStore.add(backupData); |
| | |
| | backupTransaction.oncomplete = () => { |
| | showToast('Backup criado com sucesso!'); |
| | loadBackups(); |
| | |
| | |
| | const dataStr = JSON.stringify(backupData); |
| | const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); |
| | |
| | const exportName = `pharmamanager_backup_${new Date().toISOString().slice(0, 10)}.json`; |
| | |
| | const linkElement = document.createElement('a'); |
| | linkElement.setAttribute('href', dataUri); |
| | linkElement.setAttribute('download', exportName); |
| | linkElement.click(); |
| | }; |
| | |
| | backupTransaction.onerror = () => { |
| | showToast('Erro ao salvar backup local', 'error'); |
| | }; |
| | }).catch(error => { |
| | console.error("Backup error:", error); |
| | showToast('Erro ao criar backup', 'error'); |
| | }); |
| | } |
| | |
| | function restoreBackup() { |
| | const fileInput = document.getElementById('restore-file'); |
| | const file = fileInput.files[0]; |
| | |
| | if (!file) { |
| | showToast('Selecione um arquivo de backup', 'error'); |
| | return; |
| | } |
| | |
| | const reader = new FileReader(); |
| | reader.onload = (event) => { |
| | try { |
| | const backupData = JSON.parse(event.target.result); |
| | |
| | |
| | const modal = document.getElementById('confirm-modal'); |
| | document.getElementById('confirm-title').textContent = 'Restaurar Backup'; |
| | document.getElementById('confirm-message').textContent = `Tem certeza que deseja restaurar o backup de ${new Date(backupData.timestamp).toLocaleString()}? Todos os dados atuais serão substituídos.`; |
| | |
| | document.getElementById('confirm-action').onclick = () => { |
| | performRestore(backupData); |
| | modal.classList.add('hidden'); |
| | }; |
| | |
| | modal.classList.remove('hidden'); |
| | } catch (e) { |
| | console.error("Error parsing backup file:", e); |
| | showToast('Arquivo de backup inválido', 'error'); |
| | } |
| | }; |
| | reader.readAsText(file); |
| | } |
| | |
| | function performRestore(backupData) { |
| | |
| | const transaction = db.transaction(['customers', 'products', 'sales', 'backups'], 'readwrite'); |
| | |
| | |
| | transaction.objectStore('customers').clear(); |
| | transaction.objectStore('products').clear(); |
| | transaction.objectStore('sales').clear(); |
| | |
| | |
| | backupData.customers.forEach(customer => { |
| | transaction.objectStore('customers').add(customer); |
| | }); |
| | |
| | backupData.products.forEach(product => { |
| | transaction.objectStore('products').add(product); |
| | }); |
| | |
| | backupData.sales.forEach(sale => { |
| | transaction.objectStore('sales').add(sale); |
| | }); |
| | |
| | |
| | transaction.objectStore('backups').add(backupData); |
| | |
| | transaction.oncomplete = () => { |
| | showToast('Backup restaurado com sucesso!'); |
| | loadCustomers(); |
| | updateDashboardCounts(); |
| | loadBackups(); |
| | }; |
| | |
| | transaction.onerror = () => { |
| | showToast('Erro ao restaurar backup', 'error'); |
| | }; |
| | } |
| | |
| | function loadBackups() { |
| | const transaction = db.transaction(['backups'], 'readonly'); |
| | const store = transaction.objectStore('backups'); |
| | const request = store.getAll(); |
| | |
| | request.onsuccess = () => { |
| | const backups = request.result; |
| | const tbody = document.getElementById('backup-list'); |
| | tbody.innerHTML = ''; |
| | |
| | if (backups.length === 0) { |
| | const row = document.createElement('tr'); |
| | row.innerHTML = `<td colspan="3" class="px-6 py-4 text-center text-gray-500">Nenhum backup disponível</td>`; |
| | tbody.appendChild(row); |
| | } else { |
| | |
| | backups.sort((a, b) => b.timestamp - a.timestamp); |
| | |
| | backups.forEach(backup => { |
| | const date = new Date(backup.timestamp); |
| | const size = JSON.stringify(backup).length; |
| | const sizeKB = (size / 1024).toFixed(2); |
| | |
| | const row = document.createElement('tr'); |
| | row.innerHTML = ` |
| | <td class="px-6 py-4 whitespace-nowrap">${date.toLocaleString()}</td> |
| | <td class="px-6 py-4 whitespace-nowrap">${sizeKB} KB</td> |
| | <td class="px-6 py-4 whitespace-nowrap"> |
| | <button class="download-backup text-blue-600 hover:text-blue-800 mr-3" data-id="${backup.id}"> |
| | <i class="fas fa-download"></i> Download |
| | </button> |
| | <button class="restore-from-list text-green-600 hover:text-green-800" data-id="${backup.id}"> |
| | <i class="fas fa-redo"></i> Restaurar |
| | </button> |
| | </td> |
| | `; |
| | tbody.appendChild(row); |
| | }); |
| | |
| | |
| | document.querySelectorAll('.download-backup').forEach(btn => { |
| | btn.addEventListener('click', (e) => { |
| | const id = parseInt(e.currentTarget.getAttribute('data-id')); |
| | downloadBackup(id); |
| | }); |
| | }); |
| | |
| | document.querySelectorAll('.restore-from-list').forEach(btn => { |
| | btn.addEventListener('click', (e) => { |
| | const id = parseInt(e.currentTarget.getAttribute('data-id')); |
| | confirmRestoreBackup(id); |
| | }); |
| | }); |
| | } |
| | }; |
| | } |
| | |
| | function downloadBackup(id) { |
| | const transaction = db.transaction(['backups'], 'readonly'); |
| | const store = transaction.objectStore('backups'); |
| | const request = store.get(id); |
| | |
| | request.onsuccess = () => { |
| | const backupData = request.result; |
| | const dataStr = JSON.stringify(backupData); |
| | const dataUri = 'data:application/json;charset=utf-8,'+ encodeURIComponent(dataStr); |
| | |
| | const exportName = `pharmamanager_backup_${new Date(backupData.timestamp).toISOString().slice(0, 10)}.json`; |
| | |
| | const linkElement = document.createElement('a'); |
| | linkElement.setAttribute('href', dataUri); |
| | linkElement.setAttribute('download', exportName); |
| | linkElement.click(); |
| | |
| | showToast('Download do backup iniciado'); |
| | }; |
| | } |
| | |
| | function confirmRestoreBackup(id) { |
| | const modal = document.getElementById('confirm-modal'); |
| | document.getElementById('confirm-title').textContent = 'Restaurar Backup'; |
| | document.getElementById('confirm-message').textContent = 'Tem certeza que deseja restaurar este backup? Todos os dados atuais serão substituídos.'; |
| | |
| | document.getElementById('confirm-action').onclick = () => { |
| | restoreFromBackup(id); |
| | modal.classList.add('hidden'); |
| | }; |
| | |
| | modal.classList.remove('hidden'); |
| | } |
| | |
| | function restoreFromBackup(id) { |
| | const transaction = db.transaction(['backups'], 'readonly'); |
| | const store = transaction.objectStore('backups'); |
| | const request = store.get(id); |
| | |
| | request.onsuccess = () => { |
| | performRestore(request.result); |
| | }; |
| | |
| | request.onerror = () => { |
| | showToast('Erro ao carregar backup', 'error'); |
| | }; |
| | } |
| | |
| | |
| | function updateDashboardCounts() { |
| | |
| | const customerTransaction = db.transaction(['customers'], 'readonly'); |
| | const customerStore = customerTransaction.objectStore('customers'); |
| | const customerCountRequest = customerStore.count(); |
| | |
| | customerCountRequest.onsuccess = () => { |
| | document.getElementById('customer-count').textContent = customerCountRequest.result; |
| | }; |
| | |
| | |
| | const productTransaction = db.transaction(['products'], 'readonly'); |
| | const productStore = productTransaction.objectStore('products'); |
| | const productCountRequest = productStore.count(); |
| | |
| | productCountRequest.onsuccess = () => { |
| | document.getElementById('product-count').textContent = productCountRequest.result; |
| | }; |
| | |
| | |
| | const salesTransaction = db.transaction(['sales'], 'readonly'); |
| | const salesStore = salesTransaction.objectStore('sales'); |
| | const today = new Date().toISOString().slice(0, 10); |
| | const salesTodayRequest = salesStore.index('date').getAll(today); |
| | |
| | salesTodayRequest.onsuccess = () => { |
| | document.getElementById('sales-today').textContent = salesTodayRequest.result.length; |
| | }; |
| | |
| | |
| | document.getElementById('monthly-revenue').textContent = 'R$ 0,00'; |
| | } |
| | |
| | |
| | function showToast(message, type = 'success') { |
| | const toast = document.getElementById('toast'); |
| | const toastMessage = document.getElementById('toast-message'); |
| | |
| | toastMessage.textContent = message; |
| | |
| | |
| | if (type === 'error') { |
| | toast.classList.remove('bg-green-500'); |
| | toast.classList.add('bg-red-500'); |
| | } else { |
| | toast.classList.remove('bg-red-500'); |
| | toast.classList.add('bg-green-500'); |
| | } |
| | |
| | toast.classList.remove('hidden'); |
| | |
| | |
| | setTimeout(() => { |
| | toast.classList.add('hidden'); |
| | }, 3000); |
| | } |
| | |
| | function formatCPF(cpf) { |
| | return cpf.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4'); |
| | } |
| | |
| | function formatPhone(phone) { |
| | if (phone.length === 11) { |
| | return phone.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3'); |
| | } else if (phone.length === 10) { |
| | return phone.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3'); |
| | } |
| | return phone; |
| | } |
| | |
| | |
| | document.getElementById('customer-search').addEventListener('input', (e) => { |
| | loadCustomers(1, e.target.value); |
| | }); |
| | |
| | |
| | window.addEventListener('click', (e) => { |
| | const customerModal = document.getElementById('customer-modal'); |
| | if (e.target === customerModal) { |
| | closeCustomerModal(); |
| | } |
| | |
| | const confirmModal = document.getElementById('confirm-modal'); |
| | if (e.target === confirmModal) { |
| | confirmModal.classList.add('hidden'); |
| | } |
| | }); |
| | |
| | |
| | document.getElementById('close-confirm').addEventListener('click', () => { |
| | document.getElementById('confirm-modal').classList.add('hidden'); |
| | }); |
| | |
| | document.getElementById('cancel-confirm').addEventListener('click', () => { |
| | document.getElementById('confirm-modal').classList.add('hidden'); |
| | }); |
| | </script> |
| | <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=jptec2024/teste" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| | </html> |