[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:
parent
8199d622b1
commit
ad4ab40389
@ -251,3 +251,20 @@ export const subscriptionsApi = {
|
|||||||
getTokenUsage: (limit?: number) =>
|
getTokenUsage: (limit?: number) =>
|
||||||
api.get('/subscriptions/tokens/usage', { params: { limit } }),
|
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`),
|
||||||
|
};
|
||||||
|
|||||||
@ -1,55 +1,187 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { CreditCard, AlertTriangle, CheckCircle, Clock, Plus } from 'lucide-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 clsx from 'clsx';
|
||||||
|
import { fiadosApi, customersApi } from '../lib/api';
|
||||||
|
|
||||||
const mockFiados = [
|
// Types based on backend entities
|
||||||
{
|
interface Customer {
|
||||||
id: '1',
|
id: string;
|
||||||
customer: 'Maria Lopez',
|
name: string;
|
||||||
phone: '5551234567',
|
phone?: string;
|
||||||
amount: 150.00,
|
fiadoEnabled: boolean;
|
||||||
description: 'Compra del 15/01',
|
fiadoLimit: number;
|
||||||
status: 'pending',
|
currentFiadoBalance: number;
|
||||||
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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const recentPayments = [
|
interface FiadoPayment {
|
||||||
{ customer: 'Juan Perez', amount: 200.00, date: '2024-01-15' },
|
id: string;
|
||||||
{ customer: 'Laura Sanchez', amount: 150.00, date: '2024-01-14' },
|
amount: number;
|
||||||
{ customer: 'Carlos Ruiz', amount: 75.00, date: '2024-01-13' },
|
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() {
|
export function Fiado() {
|
||||||
const [filter, setFilter] = useState<'all' | 'pending' | 'overdue'>('all');
|
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);
|
// Fetch all fiados
|
||||||
const overdueCount = mockFiados.filter(f => f.status === 'overdue').length;
|
const { data: fiadosResponse, isLoading, isError, error } = useQuery({
|
||||||
|
queryKey: ['fiados'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await fiadosApi.getAll();
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const filteredFiados = filter === 'all'
|
const fiados: Fiado[] = fiadosResponse || [];
|
||||||
? mockFiados
|
|
||||||
: mockFiados.filter(f => f.status === filter);
|
// 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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@ -58,7 +190,10 @@ export function Fiado() {
|
|||||||
<h1 className="text-2xl font-bold text-gray-900">Fiado</h1>
|
<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>
|
<p className="text-gray-500">Gestiona las cuentas de credito de tus clientes</p>
|
||||||
</div>
|
</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" />
|
<Plus className="h-5 w-5" />
|
||||||
Nuevo Fiado
|
Nuevo Fiado
|
||||||
</button>
|
</button>
|
||||||
@ -71,7 +206,7 @@ export function Fiado() {
|
|||||||
<CreditCard className="h-8 w-8 text-orange-600" />
|
<CreditCard className="h-8 w-8 text-orange-600" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-orange-700">Total Pendiente</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -80,7 +215,7 @@ export function Fiado() {
|
|||||||
<AlertTriangle className="h-8 w-8 text-red-600" />
|
<AlertTriangle className="h-8 w-8 text-red-600" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-red-700">Vencidos</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -89,7 +224,7 @@ export function Fiado() {
|
|||||||
<CheckCircle className="h-8 w-8 text-green-600" />
|
<CheckCircle className="h-8 w-8 text-green-600" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-green-700">Cobrado este mes</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,51 +252,83 @@ export function Fiado() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{filteredFiados.map((fiado) => (
|
{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 key={fiado.id} className="card">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-bold text-lg">{fiado.customer}</h3>
|
<h3 className="font-bold text-lg">{fiado.customer?.name || 'Cliente'}</h3>
|
||||||
<p className="text-sm text-gray-500">{fiado.phone}</p>
|
<p className="text-sm text-gray-500">{fiado.customer?.phone || ''}</p>
|
||||||
<p className="text-gray-600 mt-1">{fiado.description}</p>
|
<p className="text-gray-600 mt-1">{fiado.description || 'Sin descripcion'}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-2xl font-bold text-orange-600">
|
<p className="text-2xl font-bold text-orange-600">
|
||||||
${fiado.amount.toFixed(2)}
|
${Number(fiado.remainingAmount).toFixed(2)}
|
||||||
</p>
|
</p>
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium mt-1',
|
'inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium mt-1',
|
||||||
fiado.status === 'overdue'
|
overdue
|
||||||
? 'bg-red-100 text-red-700'
|
? 'bg-red-100 text-red-700'
|
||||||
|
: fiado.status === 'partial'
|
||||||
|
? 'bg-blue-100 text-blue-700'
|
||||||
: 'bg-yellow-100 text-yellow-700'
|
: 'bg-yellow-100 text-yellow-700'
|
||||||
)}>
|
)}>
|
||||||
{fiado.status === 'overdue' ? (
|
{overdue ? (
|
||||||
<AlertTriangle className="h-3 w-3" />
|
<AlertTriangle className="h-3 w-3" />
|
||||||
) : (
|
) : (
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
{fiado.status === 'overdue' ? 'Vencido' : 'Pendiente'}
|
{overdue ? 'Vencido' : fiado.status === 'partial' ? 'Parcial' : 'Pendiente'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
<span>Creado: {fiado.createdAt}</span>
|
<span>Creado: {formatDate(fiado.createdAt)}</span>
|
||||||
|
{fiado.dueDate && (
|
||||||
|
<>
|
||||||
<span className="mx-2">|</span>
|
<span className="mx-2">|</span>
|
||||||
<span>Vence: {fiado.dueDate}</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>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button className="btn-outline text-sm py-1">Enviar recordatorio</button>
|
<button
|
||||||
<button className="btn-primary text-sm py-1">Registrar pago</button>
|
onClick={() => handleOpenPayment(fiado)}
|
||||||
|
className="btn-primary text-sm py-1"
|
||||||
|
>
|
||||||
|
Registrar pago
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Recent Payments */}
|
{/* Recent Payments */}
|
||||||
<div className="card h-fit">
|
<div className="card h-fit">
|
||||||
<h2 className="font-bold text-lg mb-4">Pagos Recientes</h2>
|
<h2 className="font-bold text-lg mb-4">Pagos Recientes</h2>
|
||||||
|
{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">
|
<div className="space-y-3">
|
||||||
{recentPayments.map((payment, i) => (
|
{recentPayments.map((payment, i) => (
|
||||||
<div key={i} className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
|
<div key={i} className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
|
||||||
@ -173,8 +340,320 @@ export function Fiado() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user