[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) =>
|
||||
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 { 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>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user