diff --git a/src/lib/api.ts b/src/lib/api.ts index 6108597..481532b 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -251,3 +251,20 @@ export const subscriptionsApi = { getTokenUsage: (limit?: number) => api.get('/subscriptions/tokens/usage', { params: { limit } }), }; + +// Fiados API +export const fiadosApi = { + getAll: (customerId?: string) => + api.get('/customers/fiados/all', { params: { customerId } }), + getPending: () => api.get('/customers/fiados/pending'), + create: (data: { + customerId: string; + amount: number; + description?: string; + dueDate?: string; + saleId?: string; + }) => api.post('/customers/fiados', data), + pay: (id: string, data: { amount: number; paymentMethod?: string; notes?: string }) => + api.post(`/customers/fiados/${id}/pay`, data), + cancel: (id: string) => api.patch(`/customers/fiados/${id}/cancel`), +}; diff --git a/src/pages/Fiado.tsx b/src/pages/Fiado.tsx index 19a75c7..e9890a1 100644 --- a/src/pages/Fiado.tsx +++ b/src/pages/Fiado.tsx @@ -1,55 +1,187 @@ -import { useState } from 'react'; -import { CreditCard, AlertTriangle, CheckCircle, Clock, Plus } from 'lucide-react'; +import { useState, useMemo } from 'react'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { CreditCard, AlertTriangle, CheckCircle, Clock, Plus, Loader2, X, DollarSign } from 'lucide-react'; import clsx from 'clsx'; +import { fiadosApi, customersApi } from '../lib/api'; -const mockFiados = [ - { - id: '1', - customer: 'Maria Lopez', - phone: '5551234567', - amount: 150.00, - description: 'Compra del 15/01', - status: 'pending', - dueDate: '2024-01-30', - createdAt: '2024-01-15', - }, - { - id: '2', - customer: 'Ana Garcia', - phone: '5555555555', - amount: 320.00, - description: 'Productos varios', - status: 'overdue', - dueDate: '2024-01-10', - createdAt: '2024-01-01', - }, - { - id: '3', - customer: 'Pedro Martinez', - phone: '5553334444', - amount: 89.50, - description: 'Bebidas y botanas', - status: 'pending', - dueDate: '2024-02-01', - createdAt: '2024-01-14', - }, -]; +// Types based on backend entities +interface Customer { + id: string; + name: string; + phone?: string; + fiadoEnabled: boolean; + fiadoLimit: number; + currentFiadoBalance: number; +} -const recentPayments = [ - { customer: 'Juan Perez', amount: 200.00, date: '2024-01-15' }, - { customer: 'Laura Sanchez', amount: 150.00, date: '2024-01-14' }, - { customer: 'Carlos Ruiz', amount: 75.00, date: '2024-01-13' }, -]; +interface FiadoPayment { + id: string; + amount: number; + paymentMethod: string; + notes?: string; + createdAt: string; +} + +interface Fiado { + id: string; + customerId: string; + amount: number; + paidAmount: number; + remainingAmount: number; + description?: string; + status: 'pending' | 'partial' | 'paid' | 'cancelled'; + dueDate?: string; + createdAt: string; + paidAt?: string; + customer: Customer; + payments: FiadoPayment[]; +} + +// Helper to check if fiado is overdue +function isOverdue(fiado: Fiado): boolean { + if (fiado.status === 'paid' || fiado.status === 'cancelled') return false; + if (!fiado.dueDate) return false; + return new Date(fiado.dueDate) < new Date(); +} + +// Format date for display +function formatDate(dateStr: string): string { + return new Date(dateStr).toLocaleDateString('es-MX', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + }); +} export function Fiado() { const [filter, setFilter] = useState<'all' | 'pending' | 'overdue'>('all'); + const [showNewFiadoModal, setShowNewFiadoModal] = useState(false); + const [showPaymentModal, setShowPaymentModal] = useState(false); + const [selectedFiado, setSelectedFiado] = useState(null); + const queryClient = useQueryClient(); - const totalPending = mockFiados.reduce((sum, f) => sum + f.amount, 0); - const overdueCount = mockFiados.filter(f => f.status === 'overdue').length; + // Fetch all fiados + const { data: fiadosResponse, isLoading, isError, error } = useQuery({ + queryKey: ['fiados'], + queryFn: async () => { + const res = await fiadosApi.getAll(); + return res.data; + }, + }); - const filteredFiados = filter === 'all' - ? mockFiados - : mockFiados.filter(f => f.status === filter); + const fiados: Fiado[] = fiadosResponse || []; + + // Fetch customers for new fiado modal + const { data: customersResponse } = useQuery({ + queryKey: ['customers-fiado-enabled'], + queryFn: async () => { + const res = await customersApi.getAll(); + return res.data; + }, + enabled: showNewFiadoModal, + }); + + const customers: Customer[] = customersResponse || []; + const fiadoEnabledCustomers = customers.filter((c: Customer) => c.fiadoEnabled); + + // Calculate stats + const stats = useMemo(() => { + const activeFiados = fiados.filter(f => f.status === 'pending' || f.status === 'partial'); + const totalPending = activeFiados.reduce((sum, f) => sum + Number(f.remainingAmount), 0); + const overdueCount = activeFiados.filter(f => isOverdue(f)).length; + + // Calculate collected this month + const now = new Date(); + const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); + const collectedThisMonth = fiados.reduce((sum, f) => { + const payments = f.payments || []; + return sum + payments + .filter(p => new Date(p.createdAt) >= startOfMonth) + .reduce((pSum, p) => pSum + Number(p.amount), 0); + }, 0); + + return { totalPending, overdueCount, collectedThisMonth }; + }, [fiados]); + + // Filter fiados based on selected filter + const filteredFiados = useMemo(() => { + const activeFiados = fiados.filter(f => f.status === 'pending' || f.status === 'partial'); + + if (filter === 'all') return activeFiados; + if (filter === 'overdue') return activeFiados.filter(f => isOverdue(f)); + if (filter === 'pending') return activeFiados.filter(f => !isOverdue(f)); + return activeFiados; + }, [fiados, filter]); + + // Recent payments (last 10) + const recentPayments = useMemo(() => { + const allPayments: { customer: string; amount: number; date: string }[] = []; + + fiados.forEach(f => { + (f.payments || []).forEach(p => { + allPayments.push({ + customer: f.customer?.name || 'Cliente', + amount: Number(p.amount), + date: formatDate(p.createdAt), + }); + }); + }); + + return allPayments + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, 5); + }, [fiados]); + + // Create fiado mutation + const createFiadoMutation = useMutation({ + mutationFn: (data: { customerId: string; amount: number; description?: string; dueDate?: string }) => + fiadosApi.create(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['fiados'] }); + setShowNewFiadoModal(false); + }, + }); + + // Pay fiado mutation + const payFiadoMutation = useMutation({ + mutationFn: ({ id, data }: { id: string; data: { amount: number; paymentMethod?: string; notes?: string } }) => + fiadosApi.pay(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['fiados'] }); + setShowPaymentModal(false); + setSelectedFiado(null); + }, + }); + + // Cancel fiado mutation (available for future use) + // const cancelFiadoMutation = useMutation({ + // mutationFn: (id: string) => fiadosApi.cancel(id), + // onSuccess: () => { + // queryClient.invalidateQueries({ queryKey: ['fiados'] }); + // }, + // }); + + const handleOpenPayment = (fiado: Fiado) => { + setSelectedFiado(fiado); + setShowPaymentModal(true); + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ +

Error al cargar fiados: {(error as Error)?.message || 'Error desconocido'}

+
+ ); + } return (
@@ -58,7 +190,10 @@ export function Fiado() {

Fiado

Gestiona las cuentas de credito de tus clientes

- @@ -71,7 +206,7 @@ export function Fiado() {

Total Pendiente

-

${totalPending.toFixed(2)}

+

${stats.totalPending.toFixed(2)}

@@ -80,7 +215,7 @@ export function Fiado() {

Vencidos

-

{overdueCount} cuentas

+

{stats.overdueCount} cuentas

@@ -89,7 +224,7 @@ export function Fiado() {

Cobrado este mes

-

$425.00

+

${stats.collectedThisMonth.toFixed(2)}

@@ -117,64 +252,408 @@ export function Fiado() { ))} - {filteredFiados.map((fiado) => ( -
-
-
-

{fiado.customer}

-

{fiado.phone}

-

{fiado.description}

-
-
-

- ${fiado.amount.toFixed(2)} -

-
- {fiado.status === 'overdue' ? ( - - ) : ( - - )} - {fiado.status === 'overdue' ? 'Vencido' : 'Pendiente'} + {filteredFiados.length === 0 ? ( +
+ +

No hay fiados {filter !== 'all' ? `${filter === 'pending' ? 'pendientes' : 'vencidos'}` : 'registrados'}

+
+ ) : ( + filteredFiados.map((fiado) => { + const overdue = isOverdue(fiado); + return ( +
+
+
+

{fiado.customer?.name || 'Cliente'}

+

{fiado.customer?.phone || ''}

+

{fiado.description || 'Sin descripcion'}

+
+
+

+ ${Number(fiado.remainingAmount).toFixed(2)} +

+
+ {overdue ? ( + + ) : ( + + )} + {overdue ? 'Vencido' : fiado.status === 'partial' ? 'Parcial' : 'Pendiente'} +
+
+
+
+
+ Creado: {formatDate(fiado.createdAt)} + {fiado.dueDate && ( + <> + | + Vence: {formatDate(fiado.dueDate)} + + )} + {fiado.status === 'partial' && ( + <> + | + Pagado: ${Number(fiado.paidAmount).toFixed(2)} + + )} +
+
+ +
-
-
-
- Creado: {fiado.createdAt} - | - Vence: {fiado.dueDate} -
-
- - -
-
-
- ))} + ); + }) + )}
{/* Recent Payments */}

Pagos Recientes

-
- {recentPayments.map((payment, i) => ( -
-
-

{payment.customer}

-

{payment.date}

+ {recentPayments.length === 0 ? ( +
+ +

Sin pagos recientes

+
+ ) : ( +
+ {recentPayments.map((payment, i) => ( +
+
+

{payment.customer}

+

{payment.date}

+
+

+${payment.amount.toFixed(2)}

-

+${payment.amount.toFixed(2)}

-
- ))} -
+ ))} +
+ )}
+ + {/* New Fiado Modal */} + {showNewFiadoModal && ( + setShowNewFiadoModal(false)} + onSubmit={(data) => createFiadoMutation.mutate(data)} + isLoading={createFiadoMutation.isPending} + error={createFiadoMutation.error} + /> + )} + + {/* Payment Modal */} + {showPaymentModal && selectedFiado && ( + { + setShowPaymentModal(false); + setSelectedFiado(null); + }} + onSubmit={(data) => payFiadoMutation.mutate({ id: selectedFiado.id, data })} + isLoading={payFiadoMutation.isPending} + error={payFiadoMutation.error} + /> + )} +
+ ); +} + +// New Fiado Modal Component +function NewFiadoModal({ + customers, + onClose, + onSubmit, + isLoading, + error, +}: { + customers: Customer[]; + onClose: () => void; + onSubmit: (data: { customerId: string; amount: number; description?: string; dueDate?: string }) => void; + isLoading: boolean; + error: Error | null; +}) { + const [customerId, setCustomerId] = useState(''); + const [amount, setAmount] = useState(''); + const [description, setDescription] = useState(''); + const [dueDate, setDueDate] = useState(''); + + const selectedCustomer = customers.find(c => c.id === customerId); + const availableCredit = selectedCustomer + ? Math.max(0, selectedCustomer.fiadoLimit - selectedCustomer.currentFiadoBalance) + : 0; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit({ + customerId, + amount: parseFloat(amount), + description: description || undefined, + dueDate: dueDate || undefined, + }); + }; + + return ( +
+
+
+

Nuevo Fiado

+ +
+ + {error && ( +
+ {(error as any)?.response?.data?.message || error.message || 'Error al crear fiado'} +
+ )} + +
+
+ + + {customers.length === 0 && ( +

+ No hay clientes con fiado habilitado +

+ )} + {selectedCustomer && ( +

+ Credito disponible: ${availableCredit.toFixed(2)} +

+ )} +
+ +
+ + setAmount(e.target.value)} + className="input" + placeholder="0.00" + required + /> +
+ +
+ + setDescription(e.target.value)} + className="input" + placeholder="Ej: Compra del dia" + /> +
+ +
+ + setDueDate(e.target.value)} + className="input" + min={new Date().toISOString().split('T')[0]} + /> +
+ +
+ + +
+
+
+
+ ); +} + +// Payment Modal Component +function PaymentModal({ + fiado, + onClose, + onSubmit, + isLoading, + error, +}: { + fiado: Fiado; + onClose: () => void; + onSubmit: (data: { amount: number; paymentMethod?: string; notes?: string }) => void; + isLoading: boolean; + error: Error | null; +}) { + const [amount, setAmount] = useState(String(fiado.remainingAmount)); + const [paymentMethod, setPaymentMethod] = useState('cash'); + const [notes, setNotes] = useState(''); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSubmit({ + amount: parseFloat(amount), + paymentMethod, + notes: notes || undefined, + }); + }; + + const handlePayFull = () => { + setAmount(String(fiado.remainingAmount)); + }; + + return ( +
+
+
+

Registrar Pago

+ +
+ +
+

{fiado.customer?.name}

+

{fiado.description || 'Sin descripcion'}

+

+ Saldo pendiente: ${Number(fiado.remainingAmount).toFixed(2)} +

+
+ + {error && ( +
+ {(error as any)?.response?.data?.message || error.message || 'Error al registrar pago'} +
+ )} + +
+
+ +
+ setAmount(e.target.value)} + className="input flex-1" + placeholder="0.00" + required + /> + +
+
+ +
+ + +
+ +
+ + setNotes(e.target.value)} + className="input" + placeholder="Ej: Pago parcial" + /> +
+ +
+ + +
+
+
); }