fix: save and display professional profile address, payment methods, and docs
- DTO: add payment_methods field so whitelist pipe no longer strips it - Service upsert(): extract payment_methods and write as Prisma nested upsert instead of passing flat object - Admin detail page: fix image regex to handle Firebase URLs with query params; fix payment_methods display for 1:1 Prisma relation (object, not array) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
1e0de51ec5
commit
72777df52c
@@ -23,7 +23,8 @@ interface Professional {
|
|||||||
rate?: number; average_score?: number;
|
rate?: number; average_score?: number;
|
||||||
identification_picture?: string; certificate_picture?: string; banner_picture?: string;
|
identification_picture?: string; certificate_picture?: string; banner_picture?: string;
|
||||||
users?: User; schedules?: Schedule[];
|
users?: User; schedules?: Schedule[];
|
||||||
specializations?: Specialization[]; payment_methods?: PaymentMethod[];
|
specializations?: Specialization[];
|
||||||
|
payment_methods?: PaymentMethod | PaymentMethod[] | null;
|
||||||
}
|
}
|
||||||
interface Service { id: string; description?: string; rate?: number; status: string; day: string; }
|
interface Service { id: string; description?: string; rate?: number; status: string; day: string; }
|
||||||
|
|
||||||
@@ -440,11 +441,12 @@ export default function ProfessionalDetailPage() {
|
|||||||
banner_picture: 'Foto de perfil / Banner',
|
banner_picture: 'Foto de perfil / Banner',
|
||||||
};
|
};
|
||||||
const url = professional[field];
|
const url = professional[field];
|
||||||
|
const isImage = url && /\.(jpg|jpeg|png|webp|gif)(\?|$)/i.test(url);
|
||||||
return (
|
return (
|
||||||
<div key={field} className="space-y-2">
|
<div key={field} className="space-y-2">
|
||||||
<p className="text-sm text-muted-foreground">{labels[field]}</p>
|
<p className="text-sm text-muted-foreground">{labels[field]}</p>
|
||||||
{url ? (
|
{url ? (
|
||||||
url.match(/\.(jpg|jpeg|png|webp)$/i) ? (
|
isImage ? (
|
||||||
<a href={url} target="_blank" rel="noreferrer">
|
<a href={url} target="_blank" rel="noreferrer">
|
||||||
<img src={url} alt={labels[field]} className="rounded-lg border object-cover w-full max-h-48" />
|
<img src={url} alt={labels[field]} className="rounded-lg border object-cover w-full max-h-48" />
|
||||||
</a>
|
</a>
|
||||||
@@ -463,15 +465,19 @@ export default function ProfessionalDetailPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Métodos de pago */}
|
{/* Métodos de pago */}
|
||||||
{professional.payment_methods && professional.payment_methods.length > 0 && (
|
{professional.payment_methods && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader><CardTitle>Métodos de pago aceptados</CardTitle></CardHeader>
|
<CardHeader><CardTitle>Métodos de pago aceptados</CardTitle></CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="flex flex-wrap gap-2">
|
{(() => {
|
||||||
{['nequi', 'datafono', 'transferencia']
|
const pm = Array.isArray(professional.payment_methods)
|
||||||
.filter((k) => (professional.payment_methods![0] as any)[k])
|
? (professional.payment_methods as PaymentMethod[])[0]
|
||||||
.map((k) => <Badge key={k} variant="outline">{PAYMENT_LABELS[k]}</Badge>)}
|
: professional.payment_methods as PaymentMethod;
|
||||||
</div>
|
const active = pm ? ['nequi', 'datafono', 'transferencia'].filter((k) => (pm as any)[k]) : [];
|
||||||
|
return active.length > 0
|
||||||
|
? <div className="flex flex-wrap gap-2">{active.map((k) => <Badge key={k} variant="outline">{PAYMENT_LABELS[k]}</Badge>)}</div>
|
||||||
|
: <p className="text-sm text-muted-foreground">Ninguno configurado</p>;
|
||||||
|
})()}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsString, IsOptional, IsArray, IsNumber, IsBoolean, MinLength, Matches } from 'class-validator';
|
import { IsString, IsOptional, IsArray, IsNumber, IsBoolean, MinLength, Matches, IsObject } from 'class-validator';
|
||||||
|
|
||||||
export class CreateProfessionalDto {
|
export class CreateProfessionalDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -80,6 +80,14 @@ export class UpdateProfessionalDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
location_preferences?: string;
|
location_preferences?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
payment_methods?: {
|
||||||
|
nequi?: boolean;
|
||||||
|
datafono?: boolean;
|
||||||
|
transferencia?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ScheduleDto {
|
export class ScheduleDto {
|
||||||
|
|||||||
@@ -83,11 +83,21 @@ export class ProfessionalsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async upsert(userId: string, data: any) {
|
async upsert(userId: string, data: any) {
|
||||||
|
const { payment_methods: pm, ...profData } = data;
|
||||||
|
|
||||||
|
const buildPm = (forCreate = false) => {
|
||||||
|
if (!pm) return undefined;
|
||||||
|
const pmFields = { nequi: pm.nequi ?? false, datafono: pm.datafono ?? false, transferencia: pm.transferencia ?? false };
|
||||||
|
return forCreate ? { create: pmFields } : { upsert: { update: pmFields, create: pmFields } };
|
||||||
|
};
|
||||||
|
|
||||||
const existing = await this.prisma.professionals.findUnique({ where: { user_id: userId } });
|
const existing = await this.prisma.professionals.findUnique({ where: { user_id: userId } });
|
||||||
|
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
await this.prisma.users.update({ where: { id: userId }, data: { pro_state: 1 } });
|
await this.prisma.users.update({ where: { id: userId }, data: { pro_state: 1 } });
|
||||||
const prof = await this.prisma.professionals.create({ data: { user_id: userId, is_active: false, ...data } });
|
const createData: any = { user_id: userId, is_active: false, ...profData };
|
||||||
|
if (pm) createData.payment_methods = buildPm(true);
|
||||||
|
const prof = await this.prisma.professionals.create({ data: createData });
|
||||||
const u = await this.prisma.users.findUnique({ where: { id: userId }, select: { name: true, email: true } });
|
const u = await this.prisma.users.findUnique({ where: { id: userId }, select: { name: true, email: true } });
|
||||||
if (u?.email) this.mail.sendProfessionalSubmitted(u.name || 'Usuario', u.email, data.profession || '').catch(() => {});
|
if (u?.email) this.mail.sendProfessionalSubmitted(u.name || 'Usuario', u.email, data.profession || '').catch(() => {});
|
||||||
this.tryPush(userId, 'Solicitud recibida', 'Hemos recibido tu solicitud profesional. Te avisaremos cuando sea revisada.');
|
this.tryPush(userId, 'Solicitud recibida', 'Hemos recibido tu solicitud profesional. Te avisaremos cuando sea revisada.');
|
||||||
@@ -97,8 +107,10 @@ export class ProfessionalsService {
|
|||||||
// Re-submitting after rejection reset (pro_state=0): move back to pending
|
// Re-submitting after rejection reset (pro_state=0): move back to pending
|
||||||
const user = await this.prisma.users.findUnique({ where: { id: userId }, select: { pro_state: true, name: true, email: true } });
|
const user = await this.prisma.users.findUnique({ where: { id: userId }, select: { pro_state: true, name: true, email: true } });
|
||||||
if (user?.pro_state === 0) {
|
if (user?.pro_state === 0) {
|
||||||
|
const resubData: any = { ...profData, is_active: false };
|
||||||
|
if (pm) resubData.payment_methods = buildPm();
|
||||||
await this.prisma.$transaction([
|
await this.prisma.$transaction([
|
||||||
this.prisma.professionals.update({ where: { user_id: userId }, data: { ...data, is_active: false } }),
|
this.prisma.professionals.update({ where: { user_id: userId }, data: resubData }),
|
||||||
this.prisma.users.update({ where: { id: userId }, data: { pro_state: 1 } }),
|
this.prisma.users.update({ where: { id: userId }, data: { pro_state: 1 } }),
|
||||||
]);
|
]);
|
||||||
if (user.email) this.mail.sendProfessionalSubmitted(user.name || 'Usuario', user.email, data.profession || '').catch(() => {});
|
if (user.email) this.mail.sendProfessionalSubmitted(user.name || 'Usuario', user.email, data.profession || '').catch(() => {});
|
||||||
@@ -106,7 +118,9 @@ export class ProfessionalsService {
|
|||||||
return this.prisma.professionals.findUnique({ where: { user_id: userId } });
|
return this.prisma.professionals.findUnique({ where: { user_id: userId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.prisma.professionals.update({ where: { user_id: userId }, data });
|
const updateData: any = { ...profData };
|
||||||
|
if (pm) updateData.payment_methods = buildPm();
|
||||||
|
return this.prisma.professionals.update({ where: { user_id: userId }, data: updateData });
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSchedules(professionalId: string, schedules: any[]) {
|
async updateSchedules(professionalId: string, schedules: any[]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user