Add status management and documents view to pro detail page

- Backend: deactivate and set-pending endpoints for status control
- Admin detail: Aprobar/Rechazar/Desactivar/En revisión buttons based on current state
- Admin detail: Documentos section showing ID photo and certificate
- Admin detail: deny now confirms before deleting

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-28 17:49:28 -05:00
co-authored by Claude Sonnet 4.6
parent efb48e017f
commit 7935e48fae
3 changed files with 107 additions and 3 deletions
+73 -3
View File
@@ -21,6 +21,7 @@ interface Professional {
profession?: string; identification?: string; address?: string;
rethus_code?: string; rethus_validated?: boolean;
rate?: number; average_score?: number;
identification_picture?: string; certificate_picture?: string; banner_picture?: string;
users?: User; schedules?: Schedule[];
specializations?: Specialization[]; payment_methods?: PaymentMethod[];
}
@@ -138,10 +139,21 @@ export default function ProfessionalDetailPage() {
};
const deny = async () => {
if (!confirm('¿Rechazar esta solicitud? Se eliminará el perfil profesional.')) return;
try { await api.post(`/professionals/${id}/deny`); toast.success('Solicitud rechazada'); load(); }
catch { toast.error('Error al rechazar'); }
};
const deactivate = async () => {
try { await api.post(`/professionals/${id}/deactivate`); toast.success('Profesional desactivado'); load(); }
catch { toast.error('Error al desactivar'); }
};
const setPending = async () => {
try { await api.post(`/professionals/${id}/set-pending`); toast.success('Puesto en revisión'); load(); }
catch { toast.error('Error al cambiar estado'); }
};
if (loading) return <div className="flex items-center justify-center py-16 text-muted-foreground">Cargando...</div>;
if (error || !professional) return (
<div className="flex flex-col items-center justify-center py-16 text-destructive gap-2">
@@ -163,9 +175,22 @@ export default function ProfessionalDetailPage() {
<p className="text-sm text-muted-foreground">ID profesional: {id}</p>
</div>
</div>
<Badge variant={professional.is_active ? 'default' : 'secondary'} className="text-sm px-3 py-1">
{professional.is_active ? 'Activo' : 'Pendiente'}
</Badge>
<div className="flex items-center gap-2">
<Badge variant={professional.is_active ? 'default' : 'secondary'} className="text-sm px-3 py-1">
{professional.is_active ? 'Activo' : 'Pendiente / Inactivo'}
</Badge>
{professional.is_active ? (
<>
<Button size="sm" variant="outline" onClick={setPending}>Poner en revisión</Button>
<Button size="sm" variant="destructive" onClick={deactivate}>Desactivar</Button>
</>
) : (
<>
<Button size="sm" onClick={approve} className="bg-green-600 hover:bg-green-700 text-white">Aprobar</Button>
<Button size="sm" variant="destructive" onClick={deny}>Rechazar</Button>
</>
)}
</div>
</div>
{/* Datos del usuario */}
@@ -356,6 +381,51 @@ export default function ProfessionalDetailPage() {
</CardContent>
</Card>
{/* Documentos subidos */}
{(professional.identification_picture || professional.certificate_picture || professional.banner_picture) && (
<Card>
<CardHeader><CardTitle>Documentos y fotos</CardTitle></CardHeader>
<CardContent className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{professional.identification_picture && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Foto de cédula</p>
{professional.identification_picture.match(/\.(jpg|jpeg|png|webp)$/i) ? (
<a href={professional.identification_picture} target="_blank" rel="noreferrer">
<img src={professional.identification_picture} alt="Cédula" className="rounded-lg border object-cover w-full max-h-48" />
</a>
) : (
<a href={professional.identification_picture} target="_blank" rel="noreferrer" className="flex items-center gap-2 text-sm text-primary underline">
<ExternalLink className="h-4 w-4" /> Ver documento
</a>
)}
</div>
)}
{professional.certificate_picture && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Certificado / Diploma</p>
{professional.certificate_picture.match(/\.(jpg|jpeg|png|webp)$/i) ? (
<a href={professional.certificate_picture} target="_blank" rel="noreferrer">
<img src={professional.certificate_picture} alt="Certificado" className="rounded-lg border object-cover w-full max-h-48" />
</a>
) : (
<a href={professional.certificate_picture} target="_blank" rel="noreferrer" className="flex items-center gap-2 text-sm text-primary underline">
<ExternalLink className="h-4 w-4" /> Ver documento
</a>
)}
</div>
)}
{professional.banner_picture && (
<div className="space-y-2">
<p className="text-sm text-muted-foreground">Foto de perfil / Banner</p>
<a href={professional.banner_picture} target="_blank" rel="noreferrer">
<img src={professional.banner_picture} alt="Banner" className="rounded-lg border object-cover w-full max-h-48" />
</a>
</div>
)}
</CardContent>
</Card>
)}
{/* Métodos de pago */}
{professional.payment_methods && professional.payment_methods.length > 0 && (
<Card>