Improve the frontend and repository layer
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Plus, Minus, ShoppingCart, CreditCard, Banknote, Calculator, User, Receipt, Package, Euro, TrendingUp, Clock, ToggleLeft, ToggleRight, Settings, Zap, Wifi, WifiOff, AlertCircle, CheckCircle, Loader, Trash2, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { Button, Card, StatsGrid, StatusCard, getStatusColor } from '../../../../components/ui';
|
||||
import { Plus, Minus, ShoppingCart, CreditCard, Banknote, Calculator, User, Receipt, Package, Euro, TrendingUp, Clock, ToggleLeft, ToggleRight, Settings, Zap, Wifi, WifiOff, AlertCircle, CheckCircle, Loader, Trash2, X, ChevronRight, ChevronLeft } from 'lucide-react';
|
||||
import { Button, Card, StatusCard, getStatusColor, Badge } from '../../../../components/ui';
|
||||
import { PageHeader } from '../../../../components/layout';
|
||||
import { LoadingSpinner } from '../../../../components/ui';
|
||||
import { formatters } from '../../../../components/ui/Stats/StatsPresets';
|
||||
@@ -8,7 +8,7 @@ import { useIngredients } from '../../../../api/hooks/inventory';
|
||||
import { useTenantId } from '../../../../hooks/useTenantId';
|
||||
import { ProductType, ProductCategory, IngredientResponse } from '../../../../api/types/inventory';
|
||||
import { useToast } from '../../../../hooks/ui/useToast';
|
||||
import { usePOSConfigurationData, usePOSConfigurationManager } from '../../../../api/hooks/pos';
|
||||
import { usePOSConfigurationData, usePOSConfigurationManager, usePOSTransactions, usePOSTransactionsDashboard, usePOSTransaction } from '../../../../api/hooks/pos';
|
||||
import { POSConfiguration } from '../../../../api/types/pos';
|
||||
import { posService } from '../../../../api/services/pos';
|
||||
import { bakeryColors } from '../../../../styles/colors';
|
||||
@@ -28,11 +28,515 @@ interface CartItem {
|
||||
stock: number;
|
||||
}
|
||||
|
||||
// Transactions Section Component
|
||||
const TransactionsSection: React.FC<{ tenantId: string }> = ({ tenantId }) => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [selectedTransactionId, setSelectedTransactionId] = useState<string | null>(null);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const limit = 10;
|
||||
|
||||
// Fetch transactions
|
||||
const { data: transactionsData, isLoading: transactionsLoading } = usePOSTransactions({
|
||||
tenant_id: tenantId,
|
||||
limit,
|
||||
offset: page * limit,
|
||||
});
|
||||
|
||||
// Fetch dashboard summary
|
||||
const { data: dashboardData, isLoading: dashboardLoading } = usePOSTransactionsDashboard({
|
||||
tenant_id: tenantId,
|
||||
});
|
||||
|
||||
// Fetch selected transaction details
|
||||
const { data: selectedTransaction, isLoading: detailLoading } = usePOSTransaction(
|
||||
{
|
||||
tenant_id: tenantId,
|
||||
transaction_id: selectedTransactionId || '',
|
||||
},
|
||||
{
|
||||
enabled: !!selectedTransactionId,
|
||||
}
|
||||
);
|
||||
|
||||
const handleViewDetails = (transactionId: string) => {
|
||||
setSelectedTransactionId(transactionId);
|
||||
setShowDetailModal(true);
|
||||
};
|
||||
|
||||
const handleCloseDetail = () => {
|
||||
setShowDetailModal(false);
|
||||
setSelectedTransactionId(null);
|
||||
};
|
||||
|
||||
if (transactionsLoading || dashboardLoading) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-center h-32">
|
||||
<LoadingSpinner text="Cargando transacciones..." />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const transactions = transactionsData?.transactions || [];
|
||||
const summary = transactionsData?.summary;
|
||||
const dashboard = dashboardData;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Dashboard Stats */}
|
||||
{dashboard && (
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4 flex items-center">
|
||||
<Receipt className="w-5 h-5 mr-2 text-blue-500" />
|
||||
Resumen de Transacciones
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="text-sm text-[var(--text-secondary)] mb-1">Hoy</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">{dashboard.total_transactions_today}</div>
|
||||
<div className="text-sm text-[var(--text-tertiary)] mt-1">
|
||||
{formatters.currency(dashboard.revenue_today)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="text-sm text-[var(--text-secondary)] mb-1">Esta Semana</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">{dashboard.total_transactions_this_week}</div>
|
||||
<div className="text-sm text-[var(--text-tertiary)] mt-1">
|
||||
{formatters.currency(dashboard.revenue_this_week)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg">
|
||||
<div className="text-sm text-[var(--text-secondary)] mb-1">Este Mes</div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">{dashboard.total_transactions_this_month}</div>
|
||||
<div className="text-sm text-[var(--text-tertiary)] mt-1">
|
||||
{formatters.currency(dashboard.revenue_this_month)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Transactions List */}
|
||||
<Card className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-[var(--text-primary)] flex items-center">
|
||||
<Receipt className="w-5 h-5 mr-2 text-green-500" />
|
||||
Transacciones Recientes
|
||||
</h3>
|
||||
{summary && (
|
||||
<div className="flex items-center gap-4 text-sm text-[var(--text-secondary)]">
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle className="w-4 h-4 text-green-500" />
|
||||
<span>{summary.sync_status.synced} sincronizadas</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-4 h-4 text-yellow-500" />
|
||||
<span>{summary.sync_status.pending} pendientes</span>
|
||||
</div>
|
||||
{summary.sync_status.failed > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<AlertCircle className="w-4 h-4 text-red-500" />
|
||||
<span>{summary.sync_status.failed} fallidas</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{transactions.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<Receipt className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4 opacity-30" />
|
||||
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
||||
No hay transacciones
|
||||
</h3>
|
||||
<p className="text-[var(--text-secondary)]">
|
||||
Las transacciones sincronizadas desde tus sistemas POS aparecerán aquí
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Desktop Table View - Hidden on mobile */}
|
||||
<div className="hidden md:block overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-[var(--bg-secondary)]">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--text-primary)]">ID Transacción</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--text-primary)]">Fecha</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--text-primary)]">Total</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--text-primary)]">Método Pago</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--text-primary)]">Estado</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--text-primary)]">Sync</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-[var(--text-primary)]">Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[var(--border-primary)]">
|
||||
{transactions.map((transaction) => (
|
||||
<tr key={transaction.id} className="hover:bg-[var(--bg-secondary)] transition-colors">
|
||||
<td className="px-4 py-3 text-sm text-[var(--text-primary)] font-mono">
|
||||
{transaction.external_transaction_id}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--text-secondary)]">
|
||||
{new Date(transaction.transaction_date).toLocaleString('es-ES', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm font-semibold text-[var(--text-primary)]">
|
||||
{formatters.currency(transaction.total_amount)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-[var(--text-secondary)] capitalize">
|
||||
{transaction.payment_method || 'N/A'}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<Badge
|
||||
variant={
|
||||
transaction.status === 'completed' ? 'success' :
|
||||
transaction.status === 'pending' ? 'warning' :
|
||||
'error'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{transaction.status}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{transaction.is_synced_to_sales ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<Clock className="w-5 h-5 text-yellow-500" />
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => handleViewDetails(transaction.id)}
|
||||
className="text-sm text-blue-600 hover:text-blue-800 font-medium"
|
||||
>
|
||||
Ver detalles
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Mobile Card View - Hidden on desktop */}
|
||||
<div className="md:hidden space-y-4">
|
||||
{transactions.map((transaction) => (
|
||||
<div
|
||||
key={transaction.id}
|
||||
className="bg-[var(--bg-secondary)] rounded-lg p-4 border border-[var(--border-primary)] hover:border-[var(--border-secondary)] transition-colors cursor-pointer"
|
||||
onClick={() => handleViewDetails(transaction.id)}
|
||||
>
|
||||
{/* Header Row */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex-1">
|
||||
<div className="text-xs font-mono text-[var(--text-tertiary)] mb-1">
|
||||
{transaction.external_transaction_id}
|
||||
</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">
|
||||
{new Date(transaction.transaction_date).toLocaleString('es-ES', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{transaction.is_synced_to_sales ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<Clock className="w-5 h-5 text-yellow-500" />
|
||||
)}
|
||||
<Badge
|
||||
variant={
|
||||
transaction.status === 'completed' ? 'success' :
|
||||
transaction.status === 'pending' ? 'warning' :
|
||||
'error'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{transaction.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Amount and Payment */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-[var(--text-primary)]">
|
||||
{formatters.currency(transaction.total_amount)}
|
||||
</div>
|
||||
<div className="text-sm text-[var(--text-secondary)] capitalize mt-1">
|
||||
{transaction.payment_method || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
<ChevronRight className="w-5 h-5 text-[var(--text-tertiary)]" />
|
||||
</div>
|
||||
|
||||
{/* Items Count */}
|
||||
{transaction.items && transaction.items.length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-[var(--border-primary)] text-xs text-[var(--text-secondary)]">
|
||||
{transaction.items.length} {transaction.items.length === 1 ? 'artículo' : 'artículos'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{transactionsData && (transactionsData.has_more || page > 0) && (
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<Button
|
||||
onClick={() => setPage(Math.max(0, page - 1))}
|
||||
disabled={page === 0}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
<span className="hidden sm:inline">Anterior</span>
|
||||
</Button>
|
||||
<span className="text-sm text-[var(--text-secondary)]">
|
||||
Página {page + 1}
|
||||
</span>
|
||||
<Button
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={!transactionsData.has_more}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<span className="hidden sm:inline">Siguiente</span>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Transaction Detail Modal */}
|
||||
{showDetailModal && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-[var(--bg-primary)] rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
{/* Modal Header */}
|
||||
<div className="sticky top-0 bg-[var(--bg-primary)] border-b border-[var(--border-primary)] px-6 py-4 flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold text-[var(--text-primary)]">
|
||||
Detalles de Transacción
|
||||
</h2>
|
||||
<button
|
||||
onClick={handleCloseDetail}
|
||||
className="text-[var(--text-tertiary)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Modal Content */}
|
||||
<div className="p-6">
|
||||
{detailLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<LoadingSpinner text="Cargando detalles..." />
|
||||
</div>
|
||||
) : selectedTransaction ? (
|
||||
<div className="space-y-6">
|
||||
{/* Transaction Header */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="text-sm text-[var(--text-secondary)] mb-1">ID Transacción</div>
|
||||
<div className="font-mono text-lg text-[var(--text-primary)]">
|
||||
{selectedTransaction.external_transaction_id}
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
variant={
|
||||
selectedTransaction.status === 'completed' ? 'success' :
|
||||
selectedTransaction.status === 'pending' ? 'warning' :
|
||||
'error'
|
||||
}
|
||||
size="md"
|
||||
>
|
||||
{selectedTransaction.status}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="text-sm text-[var(--text-secondary)] mb-1">Fecha</div>
|
||||
<div className="text-sm text-[var(--text-primary)]">
|
||||
{new Date(selectedTransaction.transaction_date).toLocaleString('es-ES', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm text-[var(--text-secondary)] mb-1">Sistema POS</div>
|
||||
<div className="text-sm text-[var(--text-primary)] capitalize">
|
||||
{selectedTransaction.pos_system}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Payment Information */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">Información de Pago</h3>
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Método de pago</span>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)] capitalize">
|
||||
{selectedTransaction.payment_method || 'N/A'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Subtotal</span>
|
||||
<span className="text-sm text-[var(--text-primary)]">
|
||||
{formatters.currency(selectedTransaction.subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Impuestos</span>
|
||||
<span className="text-sm text-[var(--text-primary)]">
|
||||
{formatters.currency(selectedTransaction.tax_amount)}
|
||||
</span>
|
||||
</div>
|
||||
{selectedTransaction.discount_amount && parseFloat(String(selectedTransaction.discount_amount)) > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Descuento</span>
|
||||
<span className="text-sm text-green-600">
|
||||
-{formatters.currency(selectedTransaction.discount_amount)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedTransaction.tip_amount && parseFloat(String(selectedTransaction.tip_amount)) > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-[var(--text-secondary)]">Propina</span>
|
||||
<span className="text-sm text-[var(--text-primary)]">
|
||||
{formatters.currency(selectedTransaction.tip_amount)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-3 border-t border-[var(--border-primary)] flex items-center justify-between">
|
||||
<span className="font-semibold text-[var(--text-primary)]">Total</span>
|
||||
<span className="text-xl font-bold text-[var(--text-primary)]">
|
||||
{formatters.currency(selectedTransaction.total_amount)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Transaction Items */}
|
||||
{selectedTransaction.items && selectedTransaction.items.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[var(--text-primary)] mb-3">
|
||||
Artículos ({selectedTransaction.items.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{selectedTransaction.items.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-[var(--bg-secondary)] rounded-lg p-4 flex items-center justify-between"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-[var(--text-primary)]">
|
||||
{item.product_name}
|
||||
</div>
|
||||
{item.sku && (
|
||||
<div className="text-xs text-[var(--text-tertiary)] font-mono mt-1">
|
||||
SKU: {item.sku}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm text-[var(--text-secondary)] mt-1">
|
||||
{item.quantity} × {formatters.currency(item.unit_price)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-semibold text-[var(--text-primary)]">
|
||||
{formatters.currency(item.total_price)}
|
||||
</div>
|
||||
{item.is_synced_to_sales ? (
|
||||
<div className="text-xs text-green-600 mt-1 flex items-center justify-end gap-1">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Sincronizado
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-xs text-yellow-600 mt-1 flex items-center justify-end gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
Pendiente
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sync Status */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{selectedTransaction.is_synced_to_sales ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
) : (
|
||||
<Clock className="w-5 h-5 text-yellow-500" />
|
||||
)}
|
||||
<span className="font-medium text-[var(--text-primary)]">
|
||||
Estado de Sincronización
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-[var(--text-secondary)]">
|
||||
{selectedTransaction.is_synced_to_sales ? (
|
||||
<>
|
||||
Sincronizado exitosamente
|
||||
{selectedTransaction.sync_completed_at && (
|
||||
<span className="block mt-1">
|
||||
{new Date(selectedTransaction.sync_completed_at).toLocaleString('es-ES')}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
'Pendiente de sincronización con sistema de ventas'
|
||||
)}
|
||||
</div>
|
||||
{selectedTransaction.sync_error && (
|
||||
<div className="mt-2 text-sm text-red-600">
|
||||
Error: {selectedTransaction.sync_error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 text-[var(--text-secondary)]">
|
||||
No se encontraron detalles de la transacción
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="sticky bottom-0 bg-[var(--bg-primary)] border-t border-[var(--border-primary)] px-6 py-4">
|
||||
<Button onClick={handleCloseDetail} variant="secondary" className="w-full sm:w-auto">
|
||||
Cerrar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const POSPage: React.FC = () => {
|
||||
const [cart, setCart] = useState<CartItem[]>([]);
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [posMode, setPosMode] = useState<'manual' | 'automatic'>('manual');
|
||||
const [showPOSConfig, setShowPOSConfig] = useState(false);
|
||||
const [showStats, setShowStats] = useState(false);
|
||||
|
||||
// POS Configuration State
|
||||
@@ -48,6 +552,19 @@ const POSPage: React.FC = () => {
|
||||
const posData = usePOSConfigurationData(tenantId);
|
||||
const posManager = usePOSConfigurationManager(tenantId);
|
||||
|
||||
// Set initial POS mode based on whether there are configured integrations
|
||||
// Default to 'automatic' if POS configurations exist, otherwise 'manual'
|
||||
const [posMode, setPosMode] = useState<'manual' | 'automatic'>(() => {
|
||||
return posData.configurations.length > 0 ? 'automatic' : 'manual';
|
||||
});
|
||||
|
||||
// Update posMode when configurations change (e.g., when first config is added)
|
||||
React.useEffect(() => {
|
||||
if (!posData.isLoading && posData.configurations.length > 0 && posMode === 'manual') {
|
||||
setPosMode('automatic');
|
||||
}
|
||||
}, [posData.configurations.length, posData.isLoading]);
|
||||
|
||||
// Fetch finished products from API
|
||||
const {
|
||||
data: ingredientsData,
|
||||
@@ -59,7 +576,7 @@ const POSPage: React.FC = () => {
|
||||
});
|
||||
|
||||
// Filter for finished products and convert to POS format
|
||||
const products = useMemo(() => {
|
||||
const products = useMemo(() => {
|
||||
if (!ingredientsData) return [];
|
||||
|
||||
return ingredientsData
|
||||
@@ -68,7 +585,7 @@ const POSPage: React.FC = () => {
|
||||
id: ingredient.id,
|
||||
name: ingredient.name,
|
||||
price: Number(ingredient.average_cost) || 0,
|
||||
category: ingredient.category.toLowerCase(),
|
||||
category: ingredient.category?.toLowerCase() || 'uncategorized',
|
||||
stock: Number(ingredient.current_stock) || 0,
|
||||
ingredient: ingredient
|
||||
}))
|
||||
@@ -248,64 +765,6 @@ const POSPage: React.FC = () => {
|
||||
addToast('Venta procesada exitosamente', { type: 'success' });
|
||||
};
|
||||
|
||||
// Calculate stats for the POS dashboard
|
||||
const posStats = useMemo(() => {
|
||||
const totalProducts = products.length;
|
||||
const totalStock = products.reduce((sum, product) => sum + product.stock, 0);
|
||||
const cartValue = cart.reduce((sum, item) => sum + (item.price * item.quantity), 0);
|
||||
const cartItems = cart.reduce((sum, item) => sum + item.quantity, 0);
|
||||
const lowStockProducts = products.filter(product => product.stock <= 5).length;
|
||||
const avgProductPrice = totalProducts > 0 ? products.reduce((sum, product) => sum + product.price, 0) / totalProducts : 0;
|
||||
|
||||
return {
|
||||
totalProducts,
|
||||
totalStock,
|
||||
cartValue,
|
||||
cartItems,
|
||||
lowStockProducts,
|
||||
avgProductPrice
|
||||
};
|
||||
}, [products, cart]);
|
||||
|
||||
const stats = [
|
||||
{
|
||||
title: 'Productos Disponibles',
|
||||
value: posStats.totalProducts,
|
||||
variant: 'default' as const,
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: 'Stock Total',
|
||||
value: posStats.totalStock,
|
||||
variant: 'info' as const,
|
||||
icon: Package,
|
||||
},
|
||||
{
|
||||
title: 'Artículos en Carrito',
|
||||
value: posStats.cartItems,
|
||||
variant: 'success' as const,
|
||||
icon: ShoppingCart,
|
||||
},
|
||||
{
|
||||
title: 'Valor del Carrito',
|
||||
value: formatters.currency(posStats.cartValue),
|
||||
variant: 'success' as const,
|
||||
icon: Euro,
|
||||
},
|
||||
{
|
||||
title: 'Stock Bajo',
|
||||
value: posStats.lowStockProducts,
|
||||
variant: 'warning' as const,
|
||||
icon: Clock,
|
||||
},
|
||||
{
|
||||
title: 'Precio Promedio',
|
||||
value: formatters.currency(posStats.avgProductPrice),
|
||||
variant: 'info' as const,
|
||||
icon: TrendingUp,
|
||||
},
|
||||
];
|
||||
|
||||
// Loading and error states
|
||||
if (productsLoading || !tenantId) {
|
||||
return (
|
||||
@@ -371,47 +830,12 @@ const POSPage: React.FC = () => {
|
||||
Automático
|
||||
</span>
|
||||
</div>
|
||||
{posMode === 'automatic' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPOSConfig(!showPOSConfig)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
Configurar POS
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{posMode === 'manual' ? (
|
||||
<>
|
||||
{/* Collapsible Stats Grid */}
|
||||
<Card className="p-4">
|
||||
<button
|
||||
onClick={() => setShowStats(!showStats)}
|
||||
className="w-full flex items-center justify-between text-left"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-5 h-5 text-[var(--color-primary)]" />
|
||||
<span className="font-semibold text-[var(--text-primary)]">
|
||||
Estadísticas del POS
|
||||
</span>
|
||||
</div>
|
||||
{showStats ? (
|
||||
<ChevronUp className="w-5 h-5 text-[var(--text-tertiary)]" />
|
||||
) : (
|
||||
<ChevronDown className="w-5 h-5 text-[var(--text-tertiary)]" />
|
||||
)}
|
||||
</button>
|
||||
{showStats && (
|
||||
<div className="mt-4 pt-4 border-t border-[var(--border-primary)]">
|
||||
<StatsGrid stats={stats} columns={3} />
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Main 2-Column Layout */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Left Column: Products (2/3 width on desktop) */}
|
||||
@@ -601,6 +1025,11 @@ const POSPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Transactions Section - Only show if there are configurations */}
|
||||
{posData.configurations.length > 0 && (
|
||||
<TransactionsSection tenantId={tenantId} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user