Adiciona o frontend Next.js em frontend/ (setup, design system, autenticação/tema, layout/navegação, área master, área tenant completa: clientes, produtos, vendas, recebimentos, whatsapp, e-mail, alertas, equipe e perfil). Estende o backend de forma aditiva e autorizada: cookie httpOnly para refresh token (Etapa 13) e CRUD de equipe + edição de perfil do tenant (Etapa 18, módulo src/modules/equipe/ novo e PUT /tenant/perfil). Detalhes completos em Etapas/V2/*. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
41 lines
1.3 KiB
TypeScript
41 lines
1.3 KiB
TypeScript
import type { LucideIcon } from 'lucide-react';
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
interface StatCardProps {
|
|
label: string;
|
|
value: string;
|
|
icon?: LucideIcon;
|
|
loading?: boolean;
|
|
tone?: 'default' | 'success' | 'warning' | 'danger';
|
|
className?: string;
|
|
}
|
|
|
|
const TONE_CLASSNAMES: Record<NonNullable<StatCardProps['tone']>, string> = {
|
|
default: 'text-foreground',
|
|
success: 'text-success',
|
|
warning: 'text-warning',
|
|
danger: 'text-danger',
|
|
};
|
|
|
|
/** Card de resumo numérico (dashboards) — Etapa 15. */
|
|
export function StatCard({ label, value, icon: Icon, loading, tone = 'default', className }: StatCardProps) {
|
|
return (
|
|
<Card className={className}>
|
|
<CardHeader className="flex flex-row items-center justify-between gap-2 space-y-0">
|
|
<CardTitle className="text-sm font-medium text-muted-foreground">{label}</CardTitle>
|
|
{Icon && <Icon className="size-4 text-muted-foreground" aria-hidden />}
|
|
</CardHeader>
|
|
<CardContent>
|
|
{loading ? (
|
|
<Skeleton className="h-8 w-24" />
|
|
) : (
|
|
<p className={cn('text-2xl font-semibold tabular-nums', TONE_CLASSNAMES[tone])}>{value}</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|