570 lines
23 KiB
TypeScript
570 lines
23 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useMemo } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import {
|
|
Card, CardContent, Button, Input, Label,
|
|
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
|
|
cn,
|
|
} from '@horux/shared-ui';
|
|
import { apiClient } from '@/lib/api/client';
|
|
import { useAuthStore } from '@/stores/auth-store';
|
|
import { useContribuyenteStore } from '@/stores/contribuyente-store';
|
|
import { Upload, Download, Trash2, CheckCircle2, XCircle, Clock, AlertTriangle, MessageSquare, UserCheck } from 'lucide-react';
|
|
|
|
const MESES = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'];
|
|
const ALLOWED_MIMES = [
|
|
'application/pdf',
|
|
'application/msword',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
'application/vnd.ms-excel',
|
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
];
|
|
const ALLOWED_EXT = '.pdf,.doc,.docx,.xls,.xlsx';
|
|
const MAX_SIZE = 5 * 1024 * 1024;
|
|
const ROLES_APROBADOR = new Set(['owner', 'cfo', 'supervisor']);
|
|
|
|
interface Papeleria {
|
|
id: number;
|
|
contribuyenteId: string;
|
|
nombre: string;
|
|
descripcion: string | null;
|
|
archivoFilename: string;
|
|
archivoMime: string;
|
|
archivoSize: number;
|
|
anio: number;
|
|
mes: number;
|
|
requiereAprobacion: boolean;
|
|
estado: 'pendiente' | 'aprobado' | 'rechazado' | null;
|
|
comentarioRechazo: string | null;
|
|
requiereAprobacionCliente: boolean;
|
|
estadoCliente: 'pendiente' | 'aprobado' | 'rechazado' | null;
|
|
comentarioRechazoCliente: string | null;
|
|
subidoPor: string;
|
|
createdAt: string;
|
|
}
|
|
|
|
function fileToBase64(file: File): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const r = new FileReader();
|
|
r.onload = () => {
|
|
const result = String(r.result);
|
|
const i = result.indexOf(',');
|
|
resolve(i >= 0 ? result.substring(i + 1) : result);
|
|
};
|
|
r.onerror = reject;
|
|
r.readAsDataURL(file);
|
|
});
|
|
}
|
|
|
|
function estadoGlobal(item: Papeleria): 'sin_aprobacion' | 'pendiente' | 'aprobado' | 'rechazado' {
|
|
const reqOwner = item.requiereAprobacion;
|
|
const reqCliente = item.requiereAprobacionCliente;
|
|
const estOwner = item.estado;
|
|
const estCliente = item.estadoCliente;
|
|
|
|
if (!reqOwner && !reqCliente) return 'sin_aprobacion';
|
|
if (estOwner === 'rechazado' || estCliente === 'rechazado') return 'rechazado';
|
|
if (reqOwner && reqCliente) {
|
|
if (estOwner === 'aprobado' && estCliente === 'aprobado') return 'aprobado';
|
|
return 'pendiente';
|
|
}
|
|
if (reqOwner) return estOwner ?? 'pendiente';
|
|
return estCliente ?? 'pendiente';
|
|
}
|
|
|
|
function EstadoBadge({ item }: { item: Papeleria }) {
|
|
const global = estadoGlobal(item);
|
|
|
|
if (global === 'sin_aprobacion') {
|
|
return <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-muted text-muted-foreground">Sin aprobación</span>;
|
|
}
|
|
if (global === 'aprobado') {
|
|
return <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"><CheckCircle2 className="h-3 w-3" /> Aprobado</span>;
|
|
}
|
|
if (global === 'rechazado') {
|
|
return <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"><XCircle className="h-3 w-3" /> Rechazado</span>;
|
|
}
|
|
|
|
// Pendiente — mostrar quién falta
|
|
const faltaOwner = item.requiereAprobacion && item.estado !== 'aprobado';
|
|
const faltaCliente = item.requiereAprobacionCliente && item.estadoCliente !== 'aprobado';
|
|
let label = 'Pendiente';
|
|
if (faltaOwner && faltaCliente) label = 'Pendiente (ambos)';
|
|
else if (faltaOwner) label = 'Pendiente (owner)';
|
|
else if (faltaCliente) label = 'Pendiente (cliente)';
|
|
|
|
return <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"><Clock className="h-3 w-3" /> {label}</span>;
|
|
}
|
|
|
|
export function PapeleriaTab() {
|
|
const user = useAuthStore(s => s.user);
|
|
const { selectedContribuyenteId } = useContribuyenteStore();
|
|
const queryClient = useQueryClient();
|
|
const isCliente = user?.role === 'cliente';
|
|
const canApproveOwner = user?.role ? ROLES_APROBADOR.has(user.role) : false;
|
|
const canUpload = !isCliente;
|
|
|
|
const [showUpload, setShowUpload] = useState(false);
|
|
const [rechazoFor, setRechazoFor] = useState<Papeleria | null>(null);
|
|
const [comentarioRechazo, setComentarioRechazo] = useState('');
|
|
const [rechazoClienteFor, setRechazoClienteFor] = useState<Papeleria | null>(null);
|
|
const [comentarioRechazoCliente, setComentarioRechazoCliente] = useState('');
|
|
|
|
// Filtros
|
|
const currentYear = new Date().getFullYear();
|
|
const [filterAnio, setFilterAnio] = useState<number | ''>('');
|
|
const [filterMes, setFilterMes] = useState<number | ''>('');
|
|
const [filterEstado, setFilterEstado] = useState<string>('');
|
|
|
|
const query = useQuery<Papeleria[]>({
|
|
queryKey: ['papeleria', selectedContribuyenteId, filterAnio, filterMes, filterEstado],
|
|
queryFn: async () => {
|
|
const p = new URLSearchParams({ contribuyenteId: selectedContribuyenteId! });
|
|
if (filterAnio) p.set('anio', String(filterAnio));
|
|
if (filterMes) p.set('mes', String(filterMes));
|
|
if (filterEstado) p.set('estado', filterEstado);
|
|
const { data } = await apiClient.get<Papeleria[]>(`/papeleria?${p}`);
|
|
return data;
|
|
},
|
|
enabled: !!selectedContribuyenteId,
|
|
});
|
|
|
|
const invalidate = () => queryClient.invalidateQueries({ queryKey: ['papeleria'] });
|
|
|
|
// Upload form state
|
|
const [file, setFile] = useState<File | null>(null);
|
|
const [nombre, setNombre] = useState('');
|
|
const [descripcion, setDescripcion] = useState('');
|
|
const [anio, setAnio] = useState(currentYear);
|
|
const [mes, setMes] = useState(new Date().getMonth() + 1);
|
|
const [requiereAprobacion, setRequiereAprobacion] = useState(false);
|
|
const [requiereAprobacionCliente, setRequiereAprobacionCliente] = useState(false);
|
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
|
|
|
const resetUpload = () => {
|
|
setFile(null);
|
|
setNombre('');
|
|
setDescripcion('');
|
|
setAnio(currentYear);
|
|
setMes(new Date().getMonth() + 1);
|
|
setRequiereAprobacion(false);
|
|
setRequiereAprobacionCliente(false);
|
|
setUploadError(null);
|
|
};
|
|
|
|
const uploadMutation = useMutation({
|
|
mutationFn: async () => {
|
|
if (!file) throw new Error('Selecciona un archivo');
|
|
if (!ALLOWED_MIMES.includes(file.type)) throw new Error('Formato no permitido. Usa PDF, Word o Excel.');
|
|
if (file.size > MAX_SIZE) throw new Error('El archivo excede 5 MB.');
|
|
const base64 = await fileToBase64(file);
|
|
await apiClient.post('/papeleria', {
|
|
contribuyenteId: selectedContribuyenteId,
|
|
nombre: nombre || file.name,
|
|
descripcion: descripcion || null,
|
|
anio,
|
|
mes,
|
|
requiereAprobacion,
|
|
requiereAprobacionCliente,
|
|
archivoBase64: base64,
|
|
archivoFilename: file.name,
|
|
archivoMime: file.type,
|
|
});
|
|
},
|
|
onError: (err: any) => {
|
|
setUploadError(err?.response?.data?.message || err.message || 'Error al subir');
|
|
},
|
|
onSuccess: () => {
|
|
setShowUpload(false);
|
|
resetUpload();
|
|
invalidate();
|
|
},
|
|
});
|
|
|
|
const downloadMutation = useMutation({
|
|
mutationFn: async (item: Papeleria) => {
|
|
const res = await apiClient.get(`/papeleria/${item.id}/download`, { responseType: 'blob' });
|
|
const url = URL.createObjectURL(res.data);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = item.archivoFilename;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
});
|
|
|
|
const aprobarMutation = useMutation({
|
|
mutationFn: async (id: number) => apiClient.post(`/papeleria/${id}/aprobar`),
|
|
onSuccess: invalidate,
|
|
});
|
|
|
|
const rechazarMutation = useMutation({
|
|
mutationFn: async ({ id, comentario }: { id: number; comentario: string | null }) =>
|
|
apiClient.post(`/papeleria/${id}/rechazar`, { comentario }),
|
|
onSuccess: () => {
|
|
setRechazoFor(null);
|
|
setComentarioRechazo('');
|
|
invalidate();
|
|
},
|
|
});
|
|
|
|
const aprobarClienteMutation = useMutation({
|
|
mutationFn: async (id: number) => apiClient.post(`/papeleria/${id}/aprobar-cliente`),
|
|
onSuccess: invalidate,
|
|
});
|
|
|
|
const rechazarClienteMutation = useMutation({
|
|
mutationFn: async ({ id, comentario }: { id: number; comentario: string | null }) =>
|
|
apiClient.post(`/papeleria/${id}/rechazar-cliente`, { comentario }),
|
|
onSuccess: () => {
|
|
setRechazoClienteFor(null);
|
|
setComentarioRechazoCliente('');
|
|
invalidate();
|
|
},
|
|
});
|
|
|
|
const eliminarMutation = useMutation({
|
|
mutationFn: async (id: number) => apiClient.delete(`/papeleria/${id}`),
|
|
onSuccess: invalidate,
|
|
});
|
|
|
|
const items = query.data ?? [];
|
|
const años = useMemo(() => {
|
|
const set = new Set<number>([currentYear]);
|
|
items.forEach(i => set.add(i.anio));
|
|
return [...set].sort((a, b) => b - a);
|
|
}, [items, currentYear]);
|
|
|
|
if (!selectedContribuyenteId) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="py-8 text-center text-muted-foreground">
|
|
Selecciona un contribuyente para ver su papelería.
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Filtros + upload */}
|
|
<div className="flex items-end justify-between gap-3 flex-wrap">
|
|
<div className="flex items-end gap-2">
|
|
<div>
|
|
<Label className="text-xs">Año</Label>
|
|
<select
|
|
value={filterAnio}
|
|
onChange={e => setFilterAnio(e.target.value ? parseInt(e.target.value, 10) : '')}
|
|
className="h-9 rounded-md border bg-background px-2 text-sm"
|
|
>
|
|
<option value="">Todos</option>
|
|
{años.map(a => <option key={a} value={a}>{a}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Mes</Label>
|
|
<select
|
|
value={filterMes}
|
|
onChange={e => setFilterMes(e.target.value ? parseInt(e.target.value, 10) : '')}
|
|
className="h-9 rounded-md border bg-background px-2 text-sm"
|
|
>
|
|
<option value="">Todos</option>
|
|
{MESES.map((m, i) => <option key={i + 1} value={i + 1}>{m}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label className="text-xs">Estado</Label>
|
|
<select
|
|
value={filterEstado}
|
|
onChange={e => setFilterEstado(e.target.value)}
|
|
className="h-9 rounded-md border bg-background px-2 text-sm"
|
|
>
|
|
<option value="">Todos</option>
|
|
<option value="pendiente">Pendiente</option>
|
|
<option value="aprobado">Aprobado</option>
|
|
<option value="rechazado">Rechazado</option>
|
|
<option value="sin_aprobacion">Sin aprobación</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
{canUpload && (
|
|
<Button onClick={() => setShowUpload(true)}>
|
|
<Upload className="h-4 w-4 mr-2" /> Subir documento
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Listado */}
|
|
{query.isLoading ? (
|
|
<p className="text-sm text-muted-foreground">Cargando...</p>
|
|
) : items.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="py-8 text-center text-muted-foreground">
|
|
No hay documentos en papelería con los filtros seleccionados.
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{items.map(it => (
|
|
<Card key={it.id}>
|
|
<CardContent className="py-3 flex items-center gap-3">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<span className="text-sm font-medium">{it.nombre}</span>
|
|
<EstadoBadge item={it} />
|
|
<span className="text-xs text-muted-foreground">
|
|
{MESES[it.mes - 1]} {it.anio}
|
|
</span>
|
|
</div>
|
|
{it.descripcion && (
|
|
<p className="text-xs text-muted-foreground mt-0.5">{it.descripcion}</p>
|
|
)}
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
{it.archivoFilename} · {(it.archivoSize / 1024).toFixed(0)} KB
|
|
· subido {new Date(it.createdAt).toLocaleDateString('es-MX')}
|
|
</p>
|
|
{/* Mostrar estado detallado para no-clientes */}
|
|
{!isCliente && (it.requiereAprobacion || it.requiereAprobacionCliente) && (
|
|
<div className="flex items-center gap-2 mt-1">
|
|
{it.requiereAprobacion && (
|
|
<span className={`text-xs inline-flex items-center gap-1 ${it.estado === 'aprobado' ? 'text-green-700 dark:text-green-400' : it.estado === 'rechazado' ? 'text-red-700 dark:text-red-400' : 'text-yellow-700 dark:text-yellow-400'}`}>
|
|
<UserCheck className="h-3 w-3" /> Owner: {it.estado ?? '—'}
|
|
</span>
|
|
)}
|
|
{it.requiereAprobacionCliente && (
|
|
<span className={`text-xs inline-flex items-center gap-1 ${it.estadoCliente === 'aprobado' ? 'text-green-700 dark:text-green-400' : it.estadoCliente === 'rechazado' ? 'text-red-700 dark:text-red-400' : 'text-yellow-700 dark:text-yellow-400'}`}>
|
|
<UserCheck className="h-3 w-3" /> Cliente: {it.estadoCliente ?? '—'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
{it.estado === 'rechazado' && it.comentarioRechazo && (
|
|
<p className="text-xs mt-1 flex items-start gap-1 text-red-700 dark:text-red-400">
|
|
<MessageSquare className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
|
<span><strong>Owner:</strong> {it.comentarioRechazo}</span>
|
|
</p>
|
|
)}
|
|
{it.estadoCliente === 'rechazado' && it.comentarioRechazoCliente && (
|
|
<p className="text-xs mt-1 flex items-start gap-1 text-red-700 dark:text-red-400">
|
|
<MessageSquare className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
|
<span><strong>Cliente:</strong> {it.comentarioRechazoCliente}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-1 flex-shrink-0">
|
|
<Button variant="ghost" size="icon" onClick={() => downloadMutation.mutate(it)} title="Descargar">
|
|
<Download className="h-4 w-4" />
|
|
</Button>
|
|
{/* Botones owner/supervisor */}
|
|
{canApproveOwner && it.requiereAprobacion && it.estado === 'pendiente' && (
|
|
<>
|
|
<Button
|
|
variant="ghost" size="icon"
|
|
onClick={() => aprobarMutation.mutate(it.id)}
|
|
title="Aprobar"
|
|
>
|
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost" size="icon"
|
|
onClick={() => setRechazoFor(it)}
|
|
title="Rechazar"
|
|
>
|
|
<XCircle className="h-4 w-4 text-red-600" />
|
|
</Button>
|
|
</>
|
|
)}
|
|
{/* Botones cliente */}
|
|
{isCliente && it.requiereAprobacionCliente && it.estadoCliente === 'pendiente' && (
|
|
<>
|
|
<Button
|
|
variant="ghost" size="icon"
|
|
onClick={() => aprobarClienteMutation.mutate(it.id)}
|
|
title="Aprobar"
|
|
>
|
|
<CheckCircle2 className="h-4 w-4 text-green-600" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost" size="icon"
|
|
onClick={() => setRechazoClienteFor(it)}
|
|
title="Rechazar"
|
|
>
|
|
<XCircle className="h-4 w-4 text-red-600" />
|
|
</Button>
|
|
</>
|
|
)}
|
|
{canUpload && (
|
|
<Button
|
|
variant="ghost" size="icon"
|
|
onClick={() => confirm(`¿Eliminar "${it.nombre}"?`) && eliminarMutation.mutate(it.id)}
|
|
title="Eliminar"
|
|
>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal Upload */}
|
|
<Dialog open={showUpload} onOpenChange={(o) => { setShowUpload(o); if (!o) resetUpload(); }}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Subir documento de papelería</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<div>
|
|
<Label>Archivo (PDF, Word o Excel · máx 5 MB)</Label>
|
|
<input
|
|
type="file"
|
|
accept={ALLOWED_EXT}
|
|
onChange={e => {
|
|
const f = e.target.files?.[0] ?? null;
|
|
setFile(f);
|
|
if (f && !nombre) setNombre(f.name.replace(/\.[^.]+$/, ''));
|
|
setUploadError(null);
|
|
}}
|
|
className="block w-full text-sm border rounded-md px-3 py-2"
|
|
/>
|
|
{file && (
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{file.name} · {(file.size / 1024).toFixed(0)} KB
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<Label>Nombre</Label>
|
|
<Input value={nombre} onChange={e => setNombre(e.target.value)} placeholder="Ej. Reporte de cuentas" />
|
|
</div>
|
|
<div>
|
|
<Label>Descripción (opcional)</Label>
|
|
<Input value={descripcion} onChange={e => setDescripcion(e.target.value)} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<Label>Mes</Label>
|
|
<select
|
|
value={mes}
|
|
onChange={e => setMes(parseInt(e.target.value, 10))}
|
|
className="w-full h-10 rounded-md border bg-background px-3 text-sm"
|
|
>
|
|
{MESES.map((m, i) => <option key={i + 1} value={i + 1}>{m}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<Label>Año</Label>
|
|
<Input
|
|
type="number" min={2020} max={2100}
|
|
value={anio}
|
|
onChange={e => setAnio(parseInt(e.target.value, 10) || currentYear)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={requiereAprobacion}
|
|
onChange={e => setRequiereAprobacion(e.target.checked)}
|
|
/>
|
|
Este documento requiere aprobación de owner/supervisor
|
|
</label>
|
|
<label className="flex items-center gap-2 text-sm cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={requiereAprobacionCliente}
|
|
onChange={e => setRequiereAprobacionCliente(e.target.checked)}
|
|
/>
|
|
Este documento requiere aprobación del cliente
|
|
</label>
|
|
{uploadError && (
|
|
<p className="text-xs text-destructive flex items-start gap-1">
|
|
<AlertTriangle className="h-3 w-3 mt-0.5 flex-shrink-0" />
|
|
<span>{uploadError}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => setShowUpload(false)}>Cancelar</Button>
|
|
<Button
|
|
onClick={() => uploadMutation.mutate()}
|
|
disabled={!file || uploadMutation.isPending}
|
|
>
|
|
{uploadMutation.isPending ? 'Subiendo...' : 'Subir'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Modal Rechazo Owner */}
|
|
<Dialog open={!!rechazoFor} onOpenChange={(o) => { if (!o) { setRechazoFor(null); setComentarioRechazo(''); } }}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Rechazar documento</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<p className="text-sm">
|
|
Vas a rechazar <strong>{rechazoFor?.nombre}</strong>. El comentario es opcional.
|
|
</p>
|
|
<div>
|
|
<Label>Comentario (opcional)</Label>
|
|
<Input
|
|
value={comentarioRechazo}
|
|
onChange={e => setComentarioRechazo(e.target.value)}
|
|
placeholder="Motivo del rechazo..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => { setRechazoFor(null); setComentarioRechazo(''); }}>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
onClick={() => rechazoFor && rechazarMutation.mutate({ id: rechazoFor.id, comentario: comentarioRechazo || null })}
|
|
className={cn('bg-destructive hover:bg-destructive/90 text-destructive-foreground')}
|
|
>
|
|
Rechazar
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Modal Rechazo Cliente */}
|
|
<Dialog open={!!rechazoClienteFor} onOpenChange={(o) => { if (!o) { setRechazoClienteFor(null); setComentarioRechazoCliente(''); } }}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Rechazar documento</DialogTitle>
|
|
</DialogHeader>
|
|
<div className="space-y-3">
|
|
<p className="text-sm">
|
|
Vas a rechazar <strong>{rechazoClienteFor?.nombre}</strong>. El comentario es opcional.
|
|
</p>
|
|
<div>
|
|
<Label>Comentario (opcional)</Label>
|
|
<Input
|
|
value={comentarioRechazoCliente}
|
|
onChange={e => setComentarioRechazoCliente(e.target.value)}
|
|
placeholder="Motivo del rechazo..."
|
|
/>
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={() => { setRechazoClienteFor(null); setComentarioRechazoCliente(''); }}>
|
|
Cancelar
|
|
</Button>
|
|
<Button
|
|
onClick={() => rechazoClienteFor && rechazarClienteMutation.mutate({ id: rechazoClienteFor.id, comentario: comentarioRechazoCliente || null })}
|
|
className={cn('bg-destructive hover:bg-destructive/90 text-destructive-foreground')}
|
|
>
|
|
Rechazar
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
);
|
|
}
|