[MCH-FE] feat: Connect Fiado to real API

- Replace mock data with real API calls using React Query
- Add fiadosApi to lib/api.ts with all fiado endpoints
- Implement NewFiadoModal for creating fiados
- Implement PaymentModal for registering payments
- Add loading and error states
- Calculate stats dynamically from API data
- Show partial payment status and amount paid
- Display recent payments from actual payment records

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rckrdmrd 2026-01-20 02:16:15 -06:00
parent 8199d622b1
commit ad4ab40389
2 changed files with 592 additions and 96 deletions

View File

@ -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`),
};

View File

@ -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<Fiado | null>(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 (
<div className="flex items-center justify-center h-64">
<Loader2 className="h-12 w-12 animate-spin text-primary-600" />
</div>
);
}
if (isError) {
return (
<div className="flex flex-col items-center justify-center h-64 text-red-600">
<AlertTriangle className="h-12 w-12 mb-4" />
<p>Error al cargar fiados: {(error as Error)?.message || 'Error desconocido'}</p>
</div>
);
}
return (
<div className="space-y-6">
@ -58,7 +190,10 @@ export function Fiado() {
<h1 className="text-2xl font-bold text-gray-900">Fiado</h1>
<p className="text-gray-500">Gestiona las cuentas de credito de tus clientes</p>
</div>
<button className="btn-primary flex items-center gap-2">
<button
onClick={() => setShowNewFiadoModal(true)}
className="btn-primary flex items-center gap-2"
>
<Plus className="h-5 w-5" />
Nuevo Fiado
</button>
@ -71,7 +206,7 @@ export function Fiado() {
<CreditCard className="h-8 w-8 text-orange-600" />
<div>
<p className="text-sm text-orange-700">Total Pendiente</p>
<p className="text-2xl font-bold text-orange-800">${totalPending.toFixed(2)}</p>
<p className="text-2xl font-bold text-orange-800">${stats.totalPending.toFixed(2)}</p>
</div>
</div>
</div>
@ -80,7 +215,7 @@ export function Fiado() {
<AlertTriangle className="h-8 w-8 text-red-600" />
<div>
<p className="text-sm text-red-700">Vencidos</p>
<p className="text-2xl font-bold text-red-800">{overdueCount} cuentas</p>
<p className="text-2xl font-bold text-red-800">{stats.overdueCount} cuentas</p>
</div>
</div>
</div>
@ -89,7 +224,7 @@ export function Fiado() {
<CheckCircle className="h-8 w-8 text-green-600" />
<div>
<p className="text-sm text-green-700">Cobrado este mes</p>
<p className="text-2xl font-bold text-green-800">$425.00</p>
<p className="text-2xl font-bold text-green-800">${stats.collectedThisMonth.toFixed(2)}</p>
</div>
</div>
</div>
@ -117,64 +252,408 @@ export function Fiado() {
))}
</div>
{filteredFiados.map((fiado) => (
<div key={fiado.id} className="card">
<div className="flex items-start justify-between">
<div>
<h3 className="font-bold text-lg">{fiado.customer}</h3>
<p className="text-sm text-gray-500">{fiado.phone}</p>
<p className="text-gray-600 mt-1">{fiado.description}</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-orange-600">
${fiado.amount.toFixed(2)}
</p>
<div className={clsx(
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium mt-1',
fiado.status === 'overdue'
? 'bg-red-100 text-red-700'
: 'bg-yellow-100 text-yellow-700'
)}>
{fiado.status === 'overdue' ? (
<AlertTriangle className="h-3 w-3" />
) : (
<Clock className="h-3 w-3" />
)}
{fiado.status === 'overdue' ? 'Vencido' : 'Pendiente'}
{filteredFiados.length === 0 ? (
<div className="card text-center py-12">
<CreditCard className="h-12 w-12 mx-auto text-gray-300 mb-4" />
<p className="text-gray-500">No hay fiados {filter !== 'all' ? `${filter === 'pending' ? 'pendientes' : 'vencidos'}` : 'registrados'}</p>
</div>
) : (
filteredFiados.map((fiado) => {
const overdue = isOverdue(fiado);
return (
<div key={fiado.id} className="card">
<div className="flex items-start justify-between">
<div>
<h3 className="font-bold text-lg">{fiado.customer?.name || 'Cliente'}</h3>
<p className="text-sm text-gray-500">{fiado.customer?.phone || ''}</p>
<p className="text-gray-600 mt-1">{fiado.description || 'Sin descripcion'}</p>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-orange-600">
${Number(fiado.remainingAmount).toFixed(2)}
</p>
<div className={clsx(
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium mt-1',
overdue
? 'bg-red-100 text-red-700'
: fiado.status === 'partial'
? 'bg-blue-100 text-blue-700'
: 'bg-yellow-100 text-yellow-700'
)}>
{overdue ? (
<AlertTriangle className="h-3 w-3" />
) : (
<Clock className="h-3 w-3" />
)}
{overdue ? 'Vencido' : fiado.status === 'partial' ? 'Parcial' : 'Pendiente'}
</div>
</div>
</div>
<div className="flex items-center justify-between mt-4 pt-4 border-t">
<div className="text-sm text-gray-500">
<span>Creado: {formatDate(fiado.createdAt)}</span>
{fiado.dueDate && (
<>
<span className="mx-2">|</span>
<span>Vence: {formatDate(fiado.dueDate)}</span>
</>
)}
{fiado.status === 'partial' && (
<>
<span className="mx-2">|</span>
<span className="text-green-600">Pagado: ${Number(fiado.paidAmount).toFixed(2)}</span>
</>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => handleOpenPayment(fiado)}
className="btn-primary text-sm py-1"
>
Registrar pago
</button>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between mt-4 pt-4 border-t">
<div className="text-sm text-gray-500">
<span>Creado: {fiado.createdAt}</span>
<span className="mx-2">|</span>
<span>Vence: {fiado.dueDate}</span>
</div>
<div className="flex gap-2">
<button className="btn-outline text-sm py-1">Enviar recordatorio</button>
<button className="btn-primary text-sm py-1">Registrar pago</button>
</div>
</div>
</div>
))}
);
})
)}
</div>
{/* Recent Payments */}
<div className="card h-fit">
<h2 className="font-bold text-lg mb-4">Pagos Recientes</h2>
<div className="space-y-3">
{recentPayments.map((payment, i) => (
<div key={i} className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p className="font-medium">{payment.customer}</p>
<p className="text-sm text-gray-500">{payment.date}</p>
{recentPayments.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<DollarSign className="h-12 w-12 mx-auto mb-3 text-gray-300" />
<p>Sin pagos recientes</p>
</div>
) : (
<div className="space-y-3">
{recentPayments.map((payment, i) => (
<div key={i} className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p className="font-medium">{payment.customer}</p>
<p className="text-sm text-gray-500">{payment.date}</p>
</div>
<p className="font-bold text-green-600">+${payment.amount.toFixed(2)}</p>
</div>
<p className="font-bold text-green-600">+${payment.amount.toFixed(2)}</p>
</div>
))}
</div>
))}
</div>
)}
</div>
</div>
{/* New Fiado Modal */}
{showNewFiadoModal && (
<NewFiadoModal
customers={fiadoEnabledCustomers}
onClose={() => setShowNewFiadoModal(false)}
onSubmit={(data) => createFiadoMutation.mutate(data)}
isLoading={createFiadoMutation.isPending}
error={createFiadoMutation.error}
/>
)}
{/* Payment Modal */}
{showPaymentModal && selectedFiado && (
<PaymentModal
fiado={selectedFiado}
onClose={() => {
setShowPaymentModal(false);
setSelectedFiado(null);
}}
onSubmit={(data) => payFiadoMutation.mutate({ id: selectedFiado.id, data })}
isLoading={payFiadoMutation.isPending}
error={payFiadoMutation.error}
/>
)}
</div>
);
}
// 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 (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl max-w-md w-full p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold">Nuevo Fiado</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg">
<X className="h-5 w-5" />
</button>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{(error as any)?.response?.data?.message || error.message || 'Error al crear fiado'}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Cliente
</label>
<select
value={customerId}
onChange={(e) => setCustomerId(e.target.value)}
className="input"
required
>
<option value="">Seleccionar cliente...</option>
{customers.map((c) => (
<option key={c.id} value={c.id}>
{c.name} {c.phone ? `(${c.phone})` : ''}
</option>
))}
</select>
{customers.length === 0 && (
<p className="text-sm text-gray-500 mt-1">
No hay clientes con fiado habilitado
</p>
)}
{selectedCustomer && (
<p className="text-sm text-gray-500 mt-1">
Credito disponible: <span className="font-medium text-green-600">${availableCredit.toFixed(2)}</span>
</p>
)}
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Monto
</label>
<input
type="number"
step="0.01"
min="0.01"
max={availableCredit || undefined}
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="input"
placeholder="0.00"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Descripcion (opcional)
</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="input"
placeholder="Ej: Compra del dia"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Fecha de vencimiento (opcional)
</label>
<input
type="date"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="input"
min={new Date().toISOString().split('T')[0]}
/>
</div>
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 btn-outline"
>
Cancelar
</button>
<button
type="submit"
disabled={isLoading || !customerId || !amount}
className="flex-1 btn-primary disabled:opacity-50"
>
{isLoading ? (
<Loader2 className="h-5 w-5 animate-spin mx-auto" />
) : (
'Crear Fiado'
)}
</button>
</div>
</form>
</div>
</div>
);
}
// 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 (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl max-w-md w-full p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold">Registrar Pago</h2>
<button onClick={onClose} className="p-2 hover:bg-gray-100 rounded-lg">
<X className="h-5 w-5" />
</button>
</div>
<div className="mb-4 p-3 bg-gray-50 rounded-lg">
<p className="font-medium">{fiado.customer?.name}</p>
<p className="text-sm text-gray-500">{fiado.description || 'Sin descripcion'}</p>
<p className="text-lg font-bold text-orange-600 mt-1">
Saldo pendiente: ${Number(fiado.remainingAmount).toFixed(2)}
</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{(error as any)?.response?.data?.message || error.message || 'Error al registrar pago'}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Monto a pagar
</label>
<div className="flex gap-2">
<input
type="number"
step="0.01"
min="0.01"
max={Number(fiado.remainingAmount)}
value={amount}
onChange={(e) => setAmount(e.target.value)}
className="input flex-1"
placeholder="0.00"
required
/>
<button
type="button"
onClick={handlePayFull}
className="btn-outline text-sm whitespace-nowrap"
>
Pagar todo
</button>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Metodo de pago
</label>
<select
value={paymentMethod}
onChange={(e) => setPaymentMethod(e.target.value)}
className="input"
>
<option value="cash">Efectivo</option>
<option value="card">Tarjeta</option>
<option value="transfer">Transferencia</option>
<option value="codi">CoDi</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Notas (opcional)
</label>
<input
type="text"
value={notes}
onChange={(e) => setNotes(e.target.value)}
className="input"
placeholder="Ej: Pago parcial"
/>
</div>
<div className="flex gap-3 pt-4">
<button
type="button"
onClick={onClose}
className="flex-1 btn-outline"
>
Cancelar
</button>
<button
type="submit"
disabled={isLoading || !amount}
className="flex-1 btn-primary disabled:opacity-50"
>
{isLoading ? (
<Loader2 className="h-5 w-5 animate-spin mx-auto" />
) : (
'Registrar Pago'
)}
</button>
</div>
</form>
</div>
</div>
);
}