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