TorbatYar/frontend/modules/beauty/design-system/BeautyCards.tsx
Mortezakoohjani 6f4a484051 Migrate Beauty, Healthcare, and Accounting frontend to modular src/modules architecture.
Move business logic out of App Router into module packages, add boundary validation scripts, and keep all routes as thin re-exports without changing URLs or API behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 22:28:27 +03:30

289 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { Calendar, MapPin, Scissors, User, Building2, Sparkles, Gift, Clock } from "lucide-react";
import { Card, CardContent, Badge } from "@/components/ds";
import { cn } from "@/lib/utils";
import { BeautyStatusChip } from "./BeautyStatusChip";
import { businessFormatLabels } from "./tokens";
import type {
Appointment,
Branch,
BeautyService,
CustomerProfile,
Organization,
PackageDefinition,
StaffProfile,
} from "@/modules/beauty/services/beauty-business-api";
function CardShell({
children,
href,
className,
}: {
children: React.ReactNode;
href?: string;
className?: string;
}) {
const inner = (
<Card
className={cn(
"group border-[var(--border)] bg-[var(--surface)] shadow-[var(--shadow-sm)] transition-all duration-200 hover:shadow-[var(--shadow-md)]",
href && "cursor-pointer hover:border-[var(--beauty-accent)]/30",
className
)}
>
<CardContent className="p-5">{children}</CardContent>
</Card>
);
if (href) return <Link href={href}>{inner}</Link>;
return inner;
}
export function CenterCard({ org, href }: { org: Organization; href?: string }) {
return (
<CardShell href={href ?? `/beauty/site/center/${org.id}`}>
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[var(--beauty-accent-soft)] text-[var(--beauty-accent)]">
<Building2 className="h-6 w-6" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-lg font-semibold text-secondary">{org.name}</p>
<p className="mt-1 text-sm text-[var(--muted)]">{org.code}</p>
<div className="mt-3 flex flex-wrap gap-2">
<Badge tone="default">{businessFormatLabels[org.business_format] ?? org.business_format}</Badge>
<BeautyStatusChip status={org.status} domain="lifecycle" />
</div>
</div>
</div>
</CardShell>
);
}
export function SalonCard({ branch, href }: { branch: Branch; href?: string }) {
return (
<CardShell href={href ?? `/beauty/site/salon/${branch.id}`}>
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[var(--beauty-warm)]/15 text-[var(--beauty-accent)]">
<Sparkles className="h-6 w-6" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-lg font-semibold text-secondary">{branch.name}</p>
<p className="mt-1 text-sm text-[var(--muted)]">{branch.code}</p>
<BeautyStatusChip status={branch.status} domain="lifecycle" className="mt-3" />
</div>
</div>
</CardShell>
);
}
export function ServiceCard({ service, href }: { service: BeautyService; href?: string }) {
return (
<CardShell href={href ?? `/beauty/site/service/${service.id}`}>
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[var(--beauty-calm)]/20 text-[var(--beauty-accent)]">
<Scissors className="h-6 w-6" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-lg font-semibold text-secondary">{service.name}</p>
<p className="mt-1 text-sm text-[var(--muted)]">
{service.base_duration_minutes} دقیقه
{service.base_price != null ? ` · ${service.base_price.toLocaleString("fa-IR")}` : ""}
</p>
<BeautyStatusChip status={service.status} className="mt-3" />
</div>
</div>
</CardShell>
);
}
export function StaffCard({ staff, href }: { staff: StaffProfile; href?: string }) {
return (
<CardShell href={href ?? `/beauty/site/staff/${staff.id}`}>
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[var(--surface-muted)] text-secondary">
<User className="h-6 w-6" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-lg font-semibold text-secondary">{staff.display_name}</p>
<p className="mt-1 text-sm text-[var(--muted)]">{staff.kind}</p>
<BeautyStatusChip status={staff.status} domain="lifecycle" className="mt-3" />
</div>
</div>
</CardShell>
);
}
export function AppointmentCard({
appointment,
staffName,
serviceName,
href,
actions,
}: {
appointment: Appointment;
staffName?: string;
serviceName?: string;
href?: string;
actions?: React.ReactNode;
}) {
const start = new Date(appointment.scheduled_start);
return (
<CardShell href={href} className="relative">
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div className="min-w-0">
<div className="flex flex-wrap items-center gap-2">
<BeautyStatusChip status={appointment.status} domain="appointment" />
<span className="text-xs text-[var(--muted)]">{appointment.code}</span>
</div>
<p className="mt-2 flex items-center gap-2 text-sm font-medium text-secondary">
<Calendar className="h-4 w-4 text-[var(--beauty-accent)]" />
{start.toLocaleString("fa-IR")}
</p>
{staffName ? (
<p className="mt-1 flex items-center gap-2 text-sm text-[var(--muted)]">
<User className="h-4 w-4" />
{staffName}
</p>
) : null}
{serviceName ? (
<p className="mt-1 flex items-center gap-2 text-sm text-[var(--muted)]">
<Scissors className="h-4 w-4" />
{serviceName}
</p>
) : null}
</div>
{actions ? <div className="flex shrink-0 flex-wrap gap-2">{actions}</div> : null}
</div>
</CardShell>
);
}
export function PackageCard({ pkg, href }: { pkg: PackageDefinition; href?: string }) {
return (
<CardShell href={href}>
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[var(--beauty-accent-soft)] text-[var(--beauty-accent)]">
<Gift className="h-6 w-6" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-lg font-semibold text-secondary">{pkg.name}</p>
<p className="mt-1 text-sm text-[var(--muted)]">
{pkg.total_sessions} جلسه
{pkg.price != null ? ` · ${pkg.price.toLocaleString("fa-IR")}` : ""}
</p>
<BeautyStatusChip status={pkg.status} domain="lifecycle" className="mt-3" />
</div>
</div>
</CardShell>
);
}
export function CustomerCard({ customer, href }: { customer: CustomerProfile; href?: string }) {
return (
<CardShell href={href}>
<div className="flex items-start gap-4">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-[var(--surface-muted)] text-secondary">
<User className="h-6 w-6" />
</div>
<div className="min-w-0 flex-1">
<p className="truncate text-lg font-semibold text-secondary">{customer.display_name}</p>
<p className="mt-1 text-sm text-[var(--muted)]">{customer.mobile ?? customer.code}</p>
<BeautyStatusChip status={customer.status} domain="lifecycle" className="mt-3" />
</div>
</div>
</CardShell>
);
}
export function QueueRow({
position,
title,
subtitle,
status,
actions,
}: {
position: number;
title: string;
subtitle?: string;
status: string;
actions?: React.ReactNode;
}) {
return (
<div className="flex items-center gap-4 rounded-2xl border border-[var(--border)] bg-[var(--surface)] px-4 py-3 shadow-[var(--shadow-sm)]">
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-[var(--beauty-accent-soft)] text-sm font-bold text-[var(--beauty-accent)]">
{position}
</div>
<div className="min-w-0 flex-1">
<p className="font-medium text-secondary">{title}</p>
{subtitle ? <p className="text-sm text-[var(--muted)]">{subtitle}</p> : null}
</div>
<BeautyStatusChip status={status} />
{actions}
</div>
);
}
export function SearchResultRow({
title,
meta,
href,
}: {
title: string;
meta: string;
href: string;
}) {
return (
<Link
href={href}
className="flex items-center justify-between gap-4 rounded-2xl border border-[var(--border)] bg-[var(--surface)] px-4 py-4 transition-colors hover:bg-[var(--surface-muted)]"
>
<div>
<p className="font-medium text-secondary">{title}</p>
<p className="mt-1 flex items-center gap-1 text-sm text-[var(--muted)]">
<MapPin className="h-3.5 w-3.5" />
{meta}
</p>
</div>
</Link>
);
}
export function TimelineItem({
title,
subtitle,
time,
active,
}: {
title: string;
subtitle?: string;
time?: string;
active?: boolean;
}) {
return (
<div className="relative flex gap-4 pb-6 last:pb-0">
<div className="flex flex-col items-center">
<div
className={cn(
"h-3 w-3 rounded-full ring-4",
active
? "bg-[var(--beauty-accent)] ring-[var(--beauty-accent-soft)]"
: "bg-[var(--border)] ring-[var(--surface-muted)]"
)}
/>
<div className="mt-1 w-px flex-1 bg-[var(--border)]" />
</div>
<div className="min-w-0 flex-1 pt-0.5">
<p className="font-medium text-secondary">{title}</p>
{subtitle ? <p className="mt-0.5 text-sm text-[var(--muted)]">{subtitle}</p> : null}
{time ? (
<p className="mt-1 flex items-center gap-1 text-xs text-[var(--muted)]">
<Clock className="h-3 w-3" />
{time}
</p>
) : null}
</div>
</div>
);
}