teste / index.html
jptec2024's picture
faça um aplicativo de gerenciamento de farmacia , use indexedb como banco de dados, backup e restauração do banco de dados, cadastro de clientes - Initial Deployment
a026d50 verified
<!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">
<!-- Mobile Menu Button -->
<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>
<!-- Sidebar -->
<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>
<!-- Main Content -->
<div class="main-content ml-64 min-h-screen">
<!-- Header -->
<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>
<!-- Content Area -->
<div class="p-6">
<!-- Dashboard Content (default) -->
<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>
<!-- Customer Management Content -->
<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">
<!-- Customer data will be inserted here -->
</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>
<!-- Backup/Restore Content -->
<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">
<!-- Backup data will be inserted here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Add Customer Modal -->
<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>
<!-- Toast Notification -->
<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>
<!-- Confirmation Modal -->
<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>
// Database setup
let db;
const DB_NAME = "PharmaManagerDB";
const DB_VERSION = 1;
// Open or create database
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onupgradeneeded = (event) => {
db = event.target.result;
// Create customers store
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 });
}
// Create products store
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 });
}
// Create sales store
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 });
}
// Create backups store
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);
};
// UI Functions
document.addEventListener('DOMContentLoaded', () => {
// Mobile menu toggle
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');
});
// Navigation
document.getElementById('customer-nav').addEventListener('click', () => {
showSection('customer-content', 'Clientes');
loadCustomers();
});
document.getElementById('backup-nav').addEventListener('click', () => {
showSection('backup-content', 'Backup/Restore');
loadBackups();
});
// Customer management
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();
});
// Backup functionality
document.getElementById('create-backup-btn').addEventListener('click', () => {
createBackup();
});
document.getElementById('restore-backup-btn').addEventListener('click', () => {
restoreBackup();
});
// Initialize dashboard
showSection('dashboard-content', 'Dashboard');
});
function showSection(sectionId, title) {
// Hide all sections
document.querySelectorAll('[id$="-content"]').forEach(section => {
section.classList.add('hidden');
});
// Show selected section
document.getElementById(sectionId).classList.remove('hidden');
// Update page title
document.getElementById('page-title').textContent = title;
}
// Customer functions
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;
// Apply search filter
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))
);
}
// Pagination
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);
// Update table
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);
});
// Add event listeners to edit/delete buttons
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);
});
});
}
// Update pagination info
document.getElementById('customer-start').textContent = startIndex + 1;
document.getElementById('customer-end').textContent = Math.min(endIndex, filteredCustomers.length);
document.getElementById('customer-total').textContent = filteredCustomers.length;
// Update pagination buttons
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');
};
}
// Backup functions
function createBackup() {
// Get all data from the database
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
};
// Save backup to IndexedDB
const backupTransaction = db.transaction(['backups'], 'readwrite');
const backupStore = backupTransaction.objectStore('backups');
backupStore.add(backupData);
backupTransaction.oncomplete = () => {
showToast('Backup criado com sucesso!');
loadBackups();
// Create download link
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);
// Confirm restore
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) {
// Start a transaction for all object stores
const transaction = db.transaction(['customers', 'products', 'sales', 'backups'], 'readwrite');
// Clear existing data
transaction.objectStore('customers').clear();
transaction.objectStore('products').clear();
transaction.objectStore('sales').clear();
// Add backup data
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);
});
// Add to backups list
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 {
// Sort by timestamp (newest first)
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);
});
// Add event listeners
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');
};
}
// Dashboard functions
function updateDashboardCounts() {
// Customer count
const customerTransaction = db.transaction(['customers'], 'readonly');
const customerStore = customerTransaction.objectStore('customers');
const customerCountRequest = customerStore.count();
customerCountRequest.onsuccess = () => {
document.getElementById('customer-count').textContent = customerCountRequest.result;
};
// Product count
const productTransaction = db.transaction(['products'], 'readonly');
const productStore = productTransaction.objectStore('products');
const productCountRequest = productStore.count();
productCountRequest.onsuccess = () => {
document.getElementById('product-count').textContent = productCountRequest.result;
};
// Sales today
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;
};
// Monthly revenue (placeholder - would need actual sales data with amounts)
document.getElementById('monthly-revenue').textContent = 'R$ 0,00';
}
// Utility functions
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
const toastMessage = document.getElementById('toast-message');
toastMessage.textContent = message;
// Set color based on type
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');
// Hide after 3 seconds
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;
}
// Initialize search functionality
document.getElementById('customer-search').addEventListener('input', (e) => {
loadCustomers(1, e.target.value);
});
// Close modals when clicking outside
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');
}
});
// Close confirm modal buttons
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>