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:
co-authored by
Claude Sonnet 4.6
parent
efb48e017f
commit
7935e48fae
@@ -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>
|
||||
|
||||
@@ -82,6 +82,20 @@ export class ProfessionalsController {
|
||||
return this.pros.upsert(req.user.sub, dto);
|
||||
}
|
||||
|
||||
@Post(':id/deactivate')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
deactivate(@Param('id') id: string) {
|
||||
return this.pros.deactivate(id);
|
||||
}
|
||||
|
||||
@Post(':id/set-pending')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
setPending(@Param('id') id: string) {
|
||||
return this.pros.setPending(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
|
||||
@@ -155,6 +155,26 @@ export class ProfessionalsService {
|
||||
return { message: 'Solicitud rechazada' };
|
||||
}
|
||||
|
||||
async deactivate(id: string) {
|
||||
const prof = await this.prisma.professionals.findUnique({ where: { id } });
|
||||
if (!prof) throw new NotFoundException('Profesional no encontrado');
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.professionals.update({ where: { id }, data: { is_active: false } }),
|
||||
this.prisma.users.update({ where: { id: prof.user_id }, data: { pro_state: 3 } }),
|
||||
]);
|
||||
return { message: 'Profesional desactivado' };
|
||||
}
|
||||
|
||||
async setPending(id: string) {
|
||||
const prof = await this.prisma.professionals.findUnique({ where: { id } });
|
||||
if (!prof) throw new NotFoundException('Profesional no encontrado');
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.professionals.update({ where: { id }, data: { is_active: false } }),
|
||||
this.prisma.users.update({ where: { id: prof.user_id }, data: { pro_state: 1 } }),
|
||||
]);
|
||||
return { message: 'Profesional puesto en revisión' };
|
||||
}
|
||||
|
||||
async validateRethus(id: string) {
|
||||
const prof = await this.prisma.professionals.findUnique({ where: { id } });
|
||||
if (!prof) throw new NotFoundException('Profesional no encontrado');
|
||||
|
||||
Reference in New Issue
Block a user