| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416 |
- <script>
- import { onMount } from 'svelte';
- import { browser } from '$app/environment';
- let sales = [];
- let dateFilter = '';
- let paymentMethodFilter = '';
- let searchTerm = '';
- let sortBy = 'date';
- let sortDirection = 'desc';
- let selectedSale = null;
- let totalSales = null;
- let token = null;
- let company = null;
- let orderId = null;
- let csvDownload = [];
- let topItems = [];
- if (browser) {
- token = localStorage.getItem('token');
- company = Number(localStorage.getItem('company'));
- orderId = Number(localStorage.getItem('order'));
- }
- onMount(async () => {
- const myHeaders = new Headers();
- myHeaders.append('Authorization', `Bearer ${token}`);
- myHeaders.append('Content-Type', 'application/json');
- const raw = JSON.stringify({
- company_id: company,
- page: 1,
- limit: 10
- });
- const requestOptions = {
- method: 'POST',
- headers: myHeaders,
- body: raw,
- redirect: 'follow'
- };
- try {
- const response = await fetch('https://dev2.mixtech.dev.br/reports/get', requestOptions);
- const result = await response.json();
- csvDownload = result;
- console.log(result);
- if (result.status === 'ok' && result.data) {
- sales = result.data.orders.map((order) => ({
- id: order.order_id,
- timestamp: order.order_finished_at?.trim()
- ? order.order_finished_at
- : new Date().toISOString(),
- tableId: order.table_id,
- items: order.items.map((item) => ({
- productName: item.product_name,
- quantity: 1,
- priceAtSale: parseFloat(item.product_price)
- })),
- totalAmount: order.items.reduce((sum, item) => sum + parseFloat(item.product_price), 0),
- paymentMethod: order.order_flag
- }));
- totalSales = Number(result.data.total_sales).toFixed(2);
- topItems = result.data.top_items;
- console.log(topItems);
- } else {
- console.error('Erro na resposta da API:', result.msg);
- }
- } catch (error) {
- console.error('Erro ao buscar dados:', error);
- }
- });
- function exportarCSV() {
- if (!csvDownload.length) return;
- const headers = ['order_item_id', 'product_name', 'product_price', 'product_is_kitchen'];
- const linhas = [
- headers.join(','),
- ...csvDownload.map((pedido) => {
- const produto = pedido.product_details;
- return [
- pedido.order_item_id,
- JSON.stringify(produto.product_name ?? ''),
- produto.product_price,
- produto.product_is_kitchen
- ].join(',');
- })
- ];
- const csvContent = linhas.join('\n');
- const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.setAttribute('download', 'csvDownload.csv');
- link.click();
- URL.revokeObjectURL(url);
- }
- function formatDate(dateStr) {
- const date = new Date(dateStr);
- return `${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
- }
- function getPaymentMethodName(method) {
- return (
- {
- CASH: 'Dinheiro',
- PIX: 'Pix',
- DEBIT: 'Cartão de Débito',
- CREDIT: 'Cartão de Crédito'
- }[method] || method
- );
- }
- $: filteredSales = sales
- .filter((sale) => {
- const saleDate = new Date(sale.timestamp).toISOString().slice(0, 10);
- const includesSearchTerm = sale.items.some((item) =>
- item.productName.toLowerCase().includes(searchTerm.toLowerCase())
- );
- return (
- (!dateFilter || saleDate === dateFilter) &&
- (!paymentMethodFilter || sale.paymentMethod === paymentMethodFilter) &&
- (!searchTerm || includesSearchTerm)
- );
- })
- .sort((a, b) => {
- if (sortBy === 'date') {
- return sortDirection === 'asc'
- ? new Date(a.timestamp) - new Date(b.timestamp)
- : new Date(b.timestamp) - new Date(a.timestamp);
- } else {
- return sortDirection === 'asc'
- ? a.totalAmount - b.totalAmount
- : b.totalAmount - a.totalAmount;
- }
- });
- $: salesSummary = {
- total: filteredSales.reduce((sum, sale) => sum + sale.totalAmount, 0),
- count: filteredSales.reduce(
- (sum, sale) => sum + sale.items.reduce((itemSum, item) => itemSum + item.quantity, 0),
- 0
- ),
- byPaymentMethod: {
- cash: filteredSales
- .filter((s) => s.paymentMethod === 'CASH')
- .reduce((sum, s) => sum + s.totalAmount, 0),
- pix: filteredSales
- .filter((s) => s.paymentMethod === 'PIX')
- .reduce((sum, s) => sum + s.totalAmount, 0),
- debit: filteredSales
- .filter((s) => s.paymentMethod === 'DEBIT')
- .reduce((sum, s) => sum + s.totalAmount, 0),
- credit: filteredSales
- .filter((s) => s.paymentMethod === 'CREDIT')
- .reduce((sum, s) => sum + s.totalAmount, 0)
- },
- topProducts: Object.entries(
- filteredSales
- .flatMap((s) => s.items)
- .reduce((acc, item) => {
- if (!acc[item.productName]) acc[item.productName] = { quantity: 0, total: 0 };
- acc[item.productName].quantity += item.quantity;
- acc[item.productName].total += (item.priceAtSale || 0) * item.quantity;
- return acc;
- }, {})
- ).sort((a, b) => b[1].quantity - a[1].quantity)
- };
- function toggleSort(field) {
- if (sortBy === field) {
- sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
- } else {
- sortBy = field;
- sortDirection = 'desc';
- }
- }
- function exportToCSV() {
- const headers = ['Data', 'Mesa', 'Itens', 'Total', 'Forma de Pagamento'];
- const rows = filteredSales.map((sale) => [
- formatDate(sale.timestamp),
- `Mesa ${sale.tableId}`,
- sale.items.map((item) => `${item.quantity}x ${item.productName}`).join(', '),
- `R$ ${sale.totalAmount.toFixed(2)}`,
- getPaymentMethodName(sale.paymentMethod)
- ]);
- const csv = [headers, ...rows].map((row) => row.join(',')).join('\n');
- const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
- const url = URL.createObjectURL(blob);
- const link = document.createElement('a');
- link.href = url;
- link.download = `relatorio_vendas_${new Date().toISOString().slice(0, 10)}.csv`;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- }
- function handleCancelSale(id) {
- if (confirm('Tem certeza que deseja cancelar esta venda?')) {
- sales = sales.filter((sale) => sale.id !== id);
- selectedSale = null;
- }
- }
- </script>
- <div class="flex w-full flex-col rounded-md bg-stone-800 p-4">
- <div class="flex flex-grow flex-col">
- <!-- Cabeçalho e botões de ação -->
- <div class="mb-6 flex flex-col justify-between md:flex-row md:items-center">
- <h1 class="mb-4 text-2xl font-bold md:mb-0">Relatório de Vendas</h1>
- <div class="flex w-full flex-col gap-2 sm:flex-row sm:justify-end md:w-auto">
- <button
- on:click={exportToCSV}
- disabled={filteredSales.length === 0}
- class=" flex w-full items-center justify-center rounded-lg bg-[#D4AF37] px-4 py-2 text-[#1C1C1E] transition-colors hover:bg-[#3C3C3E] disabled:cursor-not-allowed sm:w-auto"
- >
- Exportar CSV
- </button>
- </div>
- </div>
- <div class="mb-6 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
- <div class="rounded-lg bg-[#2C2C2E] p-4">
- <h3 class="mb-2 text-sm text-[#A0A0A0]">Total em Vendas</h3>
- <p class="text-2xl font-bold text-[#D4AF37]">R$ {totalSales}</p>
- </div>
- <div class="rounded-lg bg-[#2C2C2E] p-4">
- <h3 class="mb-2 text-sm text-[#A0A0A0]">Produtos Mais Vendidos</h3>
- <div class="space-y-1">
- {#each topItems as topItem}
- <div class="flex justify-between text-sm">
- <span class="text-white">{topItem.product_name}</span>
- <span class="text-[#A0A0A0]">{topItem.sold_quantity}x</span>
- </div>
- {/each}
- </div>
- </div>
- </div>
- <div class="mb-6 rounded-lg bg-[#2C2C2E] p-4 shadow-lg">
- <div class="flex flex-col flex-wrap items-start gap-4 md:flex-row md:items-center">
- <div class="w-full flex-grow md:w-auto">
- <label for="dateFilter" class="mb-1 block text-sm font-medium text-[#A0A0A0]">Data</label>
- <div class="relative">
- <div
- class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-[#A0A0A0]"
- ></div>
- <input
- type="date"
- id="dateFilter"
- bind:value={dateFilter}
- class="block w-full rounded-md border border-[#A0A0A0]/20 bg-[#1C1C1E] py-2 pl-10 pr-3 text-white focus:outline-none focus:ring-2 focus:ring-[#D4AF37]"
- />
- </div>
- </div>
- <div class="w-full flex-grow md:w-auto">
- <label for="paymentMethodFilter" class="mb-1 block text-sm font-medium text-[#A0A0A0]"
- >Forma de Pagamento</label
- >
- <div class="relative">
- <div
- class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-[#A0A0A0]"
- ></div>
- <select
- id="paymentMethodFilter"
- bind:value={paymentMethodFilter}
- class="block w-full rounded-md border border-[#A0A0A0]/20 bg-[#1C1C1E] py-2 pl-10 pr-3 text-white focus:outline-none focus:ring-2 focus:ring-[#D4AF37]"
- >
- <option value="">Todas</option>
- <option value="CASH">Dinheiro</option>
- <option value="PIX">Pix</option>
- <option value="DEBIT">Cartão de Débito</option>
- <option value="CREDIT">Cartão de Crédito</option>
- </select>
- </div>
- </div>
- <div class="w-full flex-grow md:flex-1">
- <label for="searchTerm" class="mb-1 block text-sm font-medium text-[#A0A0A0]"
- >Buscar Produtos</label
- >
- <div class="relative">
- <div
- class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3 text-[#A0A0A0]"
- ></div>
- <input
- type="text"
- id="searchTerm"
- bind:value={searchTerm}
- placeholder="Busque por nome de produto..."
- class="block w-full rounded-md border border-[#A0A0A0]/20 bg-[#1C1C1E] py-2 pl-10 pr-3 text-white focus:outline-none focus:ring-2 focus:ring-[#D4AF37]"
- />
- </div>
- </div>
- <div class="mt-6 w-full self-end md:mt-0 md:w-auto">
- <button
- on:click={() => {
- dateFilter = '';
- paymentMethodFilter = '';
- searchTerm = '';
- }}
- class="flex w-full items-center justify-center rounded-lg bg-[#1C1C1E] px-4 py-2 transition-colors hover:bg-[#2C2C2E]"
- >
- Limpar Filtros
- </button>
- </div>
- </div>
- </div>
- <div class="hidden overflow-hidden rounded-lg bg-[#2C2C2E] shadow-lg sm:block">
- <table class="min-w-full divide-y divide-gray-700">
- <thead class="sticky top-0 z-10 bg-[#1C1C1E]">
- <tr>
- <th
- class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-[#A0A0A0]"
- >
- <button on:click={() => toggleSort('date')} class="flex items-center">
- Data/Hora
- </button>
- </th>
- <th>Mesa</th>
- <th>Itens</th>
- <th>
- <button on:click={() => toggleSort('amount')} class="flex items-center">
- Total
- </button>
- </th>
- <th>Pagamento</th>
- </tr>
- </thead>
- <tbody class="divide-y divide-gray-700 bg-[#2C2C2E]">
- {#if filteredSales.length === 0}
- <tr>
- <td colspan="6" class="px-6 py-4 text-center text-[#A0A0A0]"
- >Nenhuma venda encontrada</td
- >
- </tr>
- {:else}
- {#each filteredSales as sale}
- <tr class="cursor-pointer hover:bg-[#3C3C3E]" on:click={() => (selectedSale = sale)}>
- <td class="whitespace-nowrap px-6 py-4 text-sm">{formatDate(sale.timestamp)}</td>
- <td class="whitespace-nowrap px-6 py-4 text-sm">Mesa {sale.tableId}</td>
- <td class="px-6 py-4 text-sm">
- <div class="line-clamp-1">
- {#each sale.items as item, i}
- {#if i > 0},
- {/if}{item.quantity}x {item.productName}
- {/each}
- </div>
- </td>
- <td class="whitespace-nowrap px-6 py-4 text-sm font-medium"
- >R$ {sale.totalAmount.toFixed(2)}</td
- >
- <td class="whitespace-nowrap px-6 py-4 text-sm"
- >{getPaymentMethodName(sale.paymentMethod)}</td
- >
- </tr>
- {/each}
- {/if}
- </tbody>
- </table>
- </div>
- <div class="rounded-lg bg-[#2C2C2E] shadow-lg sm:hidden">
- <div class="divide-y divide-gray-700">
- {#if filteredSales.length === 0}
- <div class="p-4 text-center text-[#A0A0A0]">Nenhuma venda encontrada</div>
- {:else}
- {#each filteredSales as sale}
- <div
- class="flex cursor-pointer flex-col space-y-2 p-4 hover:bg-[#3C3C3E]"
- on:click={() => (selectedSale = sale)}
- >
- <div class="flex items-center justify-between">
- <span class="text-sm font-medium">{formatDate(sale.timestamp)}</span>
- <span class="text-sm font-medium">Mesa {sale.tableId}</span>
- </div>
- <div class="flex items-center justify-between">
- <span class="text-sm text-[#A0A0A0]">Total:</span>
- <span class="text-base font-bold text-white">R$ {sale.totalAmount.toFixed(2)}</span>
- </div>
- <div class="text-xs text-[#A0A0A0]">
- Itens: <span class="line-clamp-1 text-white"
- >{sale.items
- .map((item) => `${item.quantity}x ${item.productName}`)
- .join(', ')}</span
- >
- </div>
- <div class="text-xs text-[#A0A0A0]">
- Pagamento: <span class="text-white">{getPaymentMethodName(sale.paymentMethod)}</span
- >
- </div>
- <!-- <div class="mt-2 flex justify-end">
- <button
- on:click|stopPropagation={() => handleCancelSale(sale.id)}
- class="rounded-lg p-1.5 text-[#FF3B30] hover:bg-[#FF3B30]/20"
- >
- </button>
- </div> -->
- </div>
- {/each}
- {/if}
- </div>
- </div>
- </div>
- </div>
|