@snksergio/design-system 0.62.0 → 0.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-lib/ai/blocos/chart/budget-breakdown.tsx +174 -0
- package/dist-lib/ai/blocos/indice.md +43 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-com-tabela.tsx +450 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-de-tarefa-com-abas.tsx +570 -0
- package/dist-lib/ai/blocos/paneldetail/detalhe-do-registro.tsx +555 -0
- package/dist-lib/ai/exemplos/app-shell/app-shell-example.tsx +140 -0
- package/dist-lib/ai/exemplos/app-shell/index.ts +3 -0
- package/dist-lib/ai/exemplos/app-shell/nav-data.ts +97 -0
- package/dist-lib/ai/exemplos/app-shell/routes.tsx +75 -0
- package/dist-lib/ai/exemplos/chat/chat-screen.tsx +152 -0
- package/dist-lib/ai/exemplos/chat/chat-v2-mocks.ts +171 -0
- package/dist-lib/ai/exemplos/chat/chat-v2.styles.ts +23 -0
- package/dist-lib/ai/exemplos/chat/chat-v2.types.ts +81 -0
- package/dist-lib/ai/exemplos/chat/components/ChannelDot/channel-dot.tsx +28 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/conversation-actions-menu.tsx +83 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationActionsMenu/index.ts +4 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.styles.ts +22 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.tsx +203 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/conversation-column.types.ts +10 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationColumn/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.styles.ts +55 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.tsx +52 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/conversation-list-item.types.ts +7 -0
- package/dist-lib/ai/exemplos/chat/components/ConversationListItem/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/DateSeparator/date-separator.tsx +18 -0
- package/dist-lib/ai/exemplos/chat/components/DetailField/detail-field.tsx +23 -0
- package/dist-lib/ai/exemplos/chat/components/DetailSection/detail-section.tsx +41 -0
- package/dist-lib/ai/exemplos/chat/components/DetailSection/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.styles.ts +30 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.tsx +142 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/details-column.types.ts +16 -0
- package/dist-lib/ai/exemplos/chat/components/DetailsColumn/index.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.styles.ts +37 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/filter-row.tsx +45 -0
- package/dist-lib/ai/exemplos/chat/components/FilterRow/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.styles.ts +27 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.tsx +66 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-column.types.ts +13 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/filters-rail.tsx +38 -0
- package/dist-lib/ai/exemplos/chat/components/FiltersColumn/index.ts +6 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.styles.ts +37 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.tsx +29 -0
- package/dist-lib/ai/exemplos/chat/components/MessageBubble/message-bubble.types.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/PersonAvatar/index.ts +5 -0
- package/dist-lib/ai/exemplos/chat/components/PersonAvatar/person-avatar.tsx +31 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/index.ts +2 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.styles.ts +19 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.tsx +140 -0
- package/dist-lib/ai/exemplos/chat/components/QueueColumn/queue-column.types.ts +13 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/index.ts +1 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.styles.ts +30 -0
- package/dist-lib/ai/exemplos/chat/components/RailItem/rail-item.tsx +42 -0
- package/dist-lib/ai/exemplos/chat/hooks/use-resizable.ts +98 -0
- package/dist-lib/ai/exemplos/chat/index.ts +1 -0
- package/dist-lib/ai/exemplos/clientes/_table-data.ts +59 -0
- package/dist-lib/ai/exemplos/clientes/clientes-screen.tsx +505 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase-mocks.ts +117 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase.styles.ts +13 -0
- package/dist-lib/ai/exemplos/clientes/clientes-showcase.types.ts +16 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.styles.ts +28 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.tsx +303 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/detail-drawer.types.ts +14 -0
- package/dist-lib/ai/exemplos/clientes/components/DetailDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/clientes/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
- package/dist-lib/ai/exemplos/clientes/index.ts +1 -0
- package/dist-lib/ai/exemplos/edit-page/components/StepNav.tsx +81 -0
- package/dist-lib/ai/exemplos/edit-page/components/section-card.tsx +85 -0
- package/dist-lib/ai/exemplos/edit-page/edit-page-screen.tsx +234 -0
- package/dist-lib/ai/exemplos/edit-page/index.ts +1 -0
- package/dist-lib/ai/exemplos/finance/_table-data.ts +57 -0
- package/dist-lib/ai/exemplos/finance/clientes-financeiro-mocks.ts +227 -0
- package/dist-lib/ai/exemplos/finance/clientes-financeiro.types.ts +75 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase-mocks.ts +117 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase.styles.ts +13 -0
- package/dist-lib/ai/exemplos/finance/clientes-showcase.types.ts +16 -0
- package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/editar-finance-drawer.tsx +241 -0
- package/dist-lib/ai/exemplos/finance/components/EditarFinanceDrawer/index.ts +5 -0
- package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/extrato-expansion.tsx +172 -0
- package/dist-lib/ai/exemplos/finance/components/ExtratoExpansion/index.ts +1 -0
- package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/finance-detail-panel.tsx +241 -0
- package/dist-lib/ai/exemplos/finance/components/FinanceDetailPanel/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/NovoClienteDrawer/novo-cliente-drawer.tsx +207 -0
- package/dist-lib/ai/exemplos/finance/components/SacarDialog/index.ts +2 -0
- package/dist-lib/ai/exemplos/finance/components/SacarDialog/sacar-dialog.tsx +346 -0
- package/dist-lib/ai/exemplos/finance/finance-screen.tsx +821 -0
- package/dist-lib/ai/exemplos/finance/index.ts +1 -0
- package/dist-lib/ai/exemplos/gantt/_gantt-data.tsx +402 -0
- package/dist-lib/ai/exemplos/gantt/gantt-screen.tsx +512 -0
- package/dist-lib/ai/exemplos/gantt/index.ts +1 -0
- package/dist-lib/ai/exemplos/login/index.ts +1 -0
- package/dist-lib/ai/exemplos/login/login-screen.tsx +246 -0
- package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/consultor-detail-panel.tsx +158 -0
- package/dist-lib/ai/exemplos/mapa-rede/components/ConsultorDetailPanel/index.ts +2 -0
- package/dist-lib/ai/exemplos/mapa-rede/index.ts +1 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede-mocks.ts +655 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-de-rede.types.ts +53 -0
- package/dist-lib/ai/exemplos/mapa-rede/mapa-rede-screen.tsx +227 -0
- package/dist-lib/ai/exemplos/order-detail/components/ActivityTab.tsx +77 -0
- package/dist-lib/ai/exemplos/order-detail/components/AttachmentsTab.tsx +45 -0
- package/dist-lib/ai/exemplos/order-detail/components/CommentsTab.tsx +111 -0
- package/dist-lib/ai/exemplos/order-detail/components/DetailsTab.tsx +189 -0
- package/dist-lib/ai/exemplos/order-detail/components/OverviewTab.tsx +200 -0
- package/dist-lib/ai/exemplos/order-detail/components/section-card.tsx +85 -0
- package/dist-lib/ai/exemplos/order-detail/index.ts +1 -0
- package/dist-lib/ai/exemplos/order-detail/order-detail-screen.tsx +119 -0
- package/dist-lib/ai/exemplos/order-detail/order-mocks.ts +186 -0
- package/dist-lib/ai/exemplos/order-detail/order.types.ts +118 -0
- package/dist-lib/ai/indice.json +30 -15
- package/dist-lib/ai/manifest.json +31 -5
- package/dist-lib/ai/roteiros/app-builder/roteiro.md +155 -0
- package/dist-lib/ai/roteiros/auth-builder/roteiro.md +42 -0
- package/dist-lib/ai/roteiros/cards/roteiro.md +34 -0
- package/dist-lib/ai/roteiros/charts/roteiro.md +32 -0
- package/dist-lib/ai/roteiros/chat/roteiro.md +31 -0
- package/dist-lib/ai/roteiros/crud-builder/blueprint.md +63 -0
- package/dist-lib/ai/roteiros/crud-builder/entrevista.md +139 -0
- package/dist-lib/ai/roteiros/crud-builder/geracao.md +113 -0
- package/dist-lib/ai/roteiros/crud-builder/roteiro.md +89 -0
- package/dist-lib/ai/roteiros/drawers/roteiro.md +41 -0
- package/dist-lib/ai/roteiros/list-builder/blueprint.md +94 -0
- package/dist-lib/ai/roteiros/list-builder/entrevista.md +188 -0
- package/dist-lib/ai/roteiros/list-builder/geracao.md +63 -0
- package/dist-lib/ai/roteiros/list-builder/roteiro.md +91 -0
- package/dist-lib/ai/roteiros/module-replicator/roteiro.md +56 -0
- package/dist-lib/ai/roteiros/page-detail/roteiro.md +32 -0
- package/dist-lib/ai/roteiros/page-edit/roteiro.md +31 -0
- package/dist-lib/ai/roteiros/screen-composer/roteiro.md +95 -0
- package/package.json +1 -1
- /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/blueprint.md +0 -0
- /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/entrevista.md +0 -0
- /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/geracao.md +0 -0
- /package/dist-lib/ai/roteiros/{dashboard → dashboard-builder}/roteiro.md +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { ChevronDown, Pencil, Trash2 } from "lucide-react";
|
|
3
|
+
import { Avatar, AvatarFallback } from "@/components/shadcn/avatar";
|
|
4
|
+
import { Button } from "@/components/ui/Button/button";
|
|
5
|
+
import { Chip } from "@/components/ui/Chip";
|
|
6
|
+
import { FloatingPanel } from "@/components/ui/FloatingPanel";
|
|
7
|
+
import { STATUSES, CATEGORIES, AGENTS } from "../../_table-data";
|
|
8
|
+
import {
|
|
9
|
+
CATEGORY_KIND,
|
|
10
|
+
STATUS_DOT,
|
|
11
|
+
formatBRL,
|
|
12
|
+
formatShortDate,
|
|
13
|
+
} from "../../clientes-showcase-mocks";
|
|
14
|
+
import { detailDrawerStyles } from "./detail-drawer.styles";
|
|
15
|
+
import type { DetailDrawerProps } from "./detail-drawer.types";
|
|
16
|
+
|
|
17
|
+
/* ── Helpers internos ────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
function formatLongDate(ms: number): string {
|
|
20
|
+
if (!ms) return "—";
|
|
21
|
+
const d = new Date(ms);
|
|
22
|
+
return d.toLocaleDateString("pt-BR", {
|
|
23
|
+
day: "numeric",
|
|
24
|
+
month: "long",
|
|
25
|
+
year: "numeric",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type SectionId = "contact" | "meta" | "notes";
|
|
30
|
+
|
|
31
|
+
/* ── DetailField (label : value) ────────────────────────────────── */
|
|
32
|
+
|
|
33
|
+
type DetailFieldProps = {
|
|
34
|
+
label: string;
|
|
35
|
+
value: ReactNode;
|
|
36
|
+
s: ReturnType<typeof detailDrawerStyles>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function DetailField({ label, value, s }: DetailFieldProps) {
|
|
40
|
+
return (
|
|
41
|
+
<div className={s.field()}>
|
|
42
|
+
<span className={s.fieldLabel()}>{label}</span>
|
|
43
|
+
<span className={s.fieldValue()}>{value || "—"}</span>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ── DetailSection (collapsible) ─────────────────────────────────── */
|
|
49
|
+
|
|
50
|
+
type DetailSectionProps = {
|
|
51
|
+
id: SectionId;
|
|
52
|
+
title: string;
|
|
53
|
+
open: boolean;
|
|
54
|
+
onToggle: (id: SectionId) => void;
|
|
55
|
+
children: ReactNode;
|
|
56
|
+
s: ReturnType<typeof detailDrawerStyles>;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function DetailSection({
|
|
60
|
+
id,
|
|
61
|
+
title,
|
|
62
|
+
open,
|
|
63
|
+
onToggle,
|
|
64
|
+
children,
|
|
65
|
+
s,
|
|
66
|
+
}: DetailSectionProps) {
|
|
67
|
+
return (
|
|
68
|
+
<section className={s.section()}>
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
className={s.sectionHead()}
|
|
72
|
+
onClick={() => onToggle(id)}
|
|
73
|
+
aria-expanded={open}
|
|
74
|
+
>
|
|
75
|
+
<span>{title}</span>
|
|
76
|
+
<ChevronDown
|
|
77
|
+
className={[s.sectionChev(), open ? s.sectionChevOpen() : ""].join(" ")}
|
|
78
|
+
/>
|
|
79
|
+
</button>
|
|
80
|
+
{open && <div className={s.sectionBody()}>{children}</div>}
|
|
81
|
+
</section>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* ── DetailDrawer ────────────────────────────────────────────────── */
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Drawer "Detalhes do cliente" — usa `<FloatingPanel>` do DS (non-modal,
|
|
89
|
+
* resizable, maximizable, responsive). Replica visual do `TblDetailsPanel`
|
|
90
|
+
* do sandbox /design-and-table-v2.
|
|
91
|
+
*/
|
|
92
|
+
export function DetailDrawer({
|
|
93
|
+
row,
|
|
94
|
+
onClose,
|
|
95
|
+
onEdit,
|
|
96
|
+
onDelete,
|
|
97
|
+
onSave,
|
|
98
|
+
}: DetailDrawerProps) {
|
|
99
|
+
const s = detailDrawerStyles();
|
|
100
|
+
const [openSections, setOpenSections] = useState<Set<SectionId>>(
|
|
101
|
+
() => new Set(["contact", "meta", "notes"]),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const toggle = (id: SectionId) => {
|
|
105
|
+
setOpenSections((prev) => {
|
|
106
|
+
const next = new Set(prev);
|
|
107
|
+
if (next.has(id)) next.delete(id);
|
|
108
|
+
else next.add(id);
|
|
109
|
+
return next;
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Mantém ordem dos hooks estável independente de `row` — depois do toggle.
|
|
114
|
+
if (!row) return null;
|
|
115
|
+
|
|
116
|
+
const status = STATUSES[row.statusId];
|
|
117
|
+
const cat = CATEGORIES[row.categoryId as keyof typeof CATEGORIES];
|
|
118
|
+
const agent = AGENTS[row.agentId as keyof typeof AGENTS];
|
|
119
|
+
const agentInitials =
|
|
120
|
+
agent?.name.split(" ").map((p) => p[0]).join("").slice(0, 2) ?? "?";
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<FloatingPanel
|
|
124
|
+
open={row !== null}
|
|
125
|
+
onOpenChange={(open) => {
|
|
126
|
+
if (!open) onClose();
|
|
127
|
+
}}
|
|
128
|
+
side="right"
|
|
129
|
+
size="md"
|
|
130
|
+
resizable
|
|
131
|
+
maximizable
|
|
132
|
+
// Sections internas (DetailSection) gerenciam o próprio padding edge-to-edge.
|
|
133
|
+
bodyPadded={false}
|
|
134
|
+
resizableStorageKey="clientes-showcase.detail-drawer.width"
|
|
135
|
+
titleSlot={
|
|
136
|
+
<div className="flex items-center gap-gp-md min-w-0">
|
|
137
|
+
<Avatar
|
|
138
|
+
className="size-[40px] shrink-0"
|
|
139
|
+
style={{ background: row.avatarColor }}
|
|
140
|
+
>
|
|
141
|
+
<AvatarFallback className="bg-transparent text-white text-body-md font-bold">
|
|
142
|
+
{row.initials}
|
|
143
|
+
</AvatarFallback>
|
|
144
|
+
</Avatar>
|
|
145
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
146
|
+
<div className="text-body-md font-semibold text-fg-default whitespace-nowrap overflow-hidden text-ellipsis">
|
|
147
|
+
{row.name}
|
|
148
|
+
</div>
|
|
149
|
+
<div className="flex items-center gap-[6px] mt-[2px] text-body-xs font-normal text-fg-muted">
|
|
150
|
+
<span>{row.id}</span>
|
|
151
|
+
<span className="opacity-50">·</span>
|
|
152
|
+
<span className="inline-flex items-center gap-[6px]">
|
|
153
|
+
<span
|
|
154
|
+
className="inline-block size-[8px] rounded-radius-full shrink-0"
|
|
155
|
+
style={{ background: STATUS_DOT[row.statusId] ?? status?.color }}
|
|
156
|
+
aria-hidden
|
|
157
|
+
/>
|
|
158
|
+
{status?.label}
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
}
|
|
164
|
+
headerActions={
|
|
165
|
+
<>
|
|
166
|
+
<Button
|
|
167
|
+
variant="soft"
|
|
168
|
+
color="secondary"
|
|
169
|
+
size="icon-sm"
|
|
170
|
+
aria-label="Editar"
|
|
171
|
+
onClick={() => onEdit?.(row)}
|
|
172
|
+
>
|
|
173
|
+
<Pencil />
|
|
174
|
+
</Button>
|
|
175
|
+
<Button
|
|
176
|
+
variant="soft"
|
|
177
|
+
color="critical"
|
|
178
|
+
size="icon-sm"
|
|
179
|
+
aria-label="Excluir"
|
|
180
|
+
onClick={() => onDelete?.(row)}
|
|
181
|
+
>
|
|
182
|
+
<Trash2 />
|
|
183
|
+
</Button>
|
|
184
|
+
</>
|
|
185
|
+
}
|
|
186
|
+
footer={
|
|
187
|
+
<>
|
|
188
|
+
<Button variant="outline" color="secondary" size="sm" onClick={onClose}>
|
|
189
|
+
Cancelar
|
|
190
|
+
</Button>
|
|
191
|
+
<Button
|
|
192
|
+
variant="filled"
|
|
193
|
+
color="primary"
|
|
194
|
+
size="sm"
|
|
195
|
+
onClick={() => onSave?.(row)}
|
|
196
|
+
>
|
|
197
|
+
Salvar alterações
|
|
198
|
+
</Button>
|
|
199
|
+
</>
|
|
200
|
+
}
|
|
201
|
+
>
|
|
202
|
+
<DetailSection
|
|
203
|
+
id="contact"
|
|
204
|
+
title="Contato"
|
|
205
|
+
open={openSections.has("contact")}
|
|
206
|
+
onToggle={toggle}
|
|
207
|
+
s={s}
|
|
208
|
+
>
|
|
209
|
+
<DetailField
|
|
210
|
+
label="Email"
|
|
211
|
+
value={
|
|
212
|
+
<a
|
|
213
|
+
href={`mailto:${row.email}`}
|
|
214
|
+
className="text-fg-brand hover:underline"
|
|
215
|
+
>
|
|
216
|
+
{row.email}
|
|
217
|
+
</a>
|
|
218
|
+
}
|
|
219
|
+
s={s}
|
|
220
|
+
/>
|
|
221
|
+
<DetailField
|
|
222
|
+
label="Telefone"
|
|
223
|
+
value={
|
|
224
|
+
<a
|
|
225
|
+
href={`tel:${row.phone.replace(/\D/g, "")}`}
|
|
226
|
+
className="text-fg-brand hover:underline"
|
|
227
|
+
>
|
|
228
|
+
{row.phone}
|
|
229
|
+
</a>
|
|
230
|
+
}
|
|
231
|
+
s={s}
|
|
232
|
+
/>
|
|
233
|
+
<DetailField label="Localização" value={row.location} s={s} />
|
|
234
|
+
</DetailSection>
|
|
235
|
+
|
|
236
|
+
<DetailSection
|
|
237
|
+
id="meta"
|
|
238
|
+
title="Atendimento"
|
|
239
|
+
open={openSections.has("meta")}
|
|
240
|
+
onToggle={toggle}
|
|
241
|
+
s={s}
|
|
242
|
+
>
|
|
243
|
+
<DetailField label="ID" value={row.id} s={s} />
|
|
244
|
+
<DetailField
|
|
245
|
+
label="Categoria"
|
|
246
|
+
value={
|
|
247
|
+
cat ? (
|
|
248
|
+
<Chip
|
|
249
|
+
color={CATEGORY_KIND[row.categoryId] ?? "info"}
|
|
250
|
+
variant="soft"
|
|
251
|
+
size="sm"
|
|
252
|
+
shape="pill"
|
|
253
|
+
>
|
|
254
|
+
{cat.label}
|
|
255
|
+
</Chip>
|
|
256
|
+
) : (
|
|
257
|
+
"—"
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
s={s}
|
|
261
|
+
/>
|
|
262
|
+
<DetailField
|
|
263
|
+
label="Atribuído a"
|
|
264
|
+
value={
|
|
265
|
+
agent ? (
|
|
266
|
+
<span className="inline-flex items-center gap-gp-sm">
|
|
267
|
+
<Avatar
|
|
268
|
+
className="size-[22px]"
|
|
269
|
+
style={{ background: agent.color }}
|
|
270
|
+
>
|
|
271
|
+
<AvatarFallback className="bg-transparent text-white text-caption-xs font-bold">
|
|
272
|
+
{agentInitials}
|
|
273
|
+
</AvatarFallback>
|
|
274
|
+
</Avatar>
|
|
275
|
+
{agent.name}
|
|
276
|
+
</span>
|
|
277
|
+
) : (
|
|
278
|
+
"—"
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
s={s}
|
|
282
|
+
/>
|
|
283
|
+
<DetailField label="Valor" value={formatBRL(row.value)} s={s} />
|
|
284
|
+
<DetailField label="Criado em" value={formatLongDate(row.createdAt)} s={s} />
|
|
285
|
+
<DetailField
|
|
286
|
+
label="Último contato"
|
|
287
|
+
value={formatShortDate(row.lastContact)}
|
|
288
|
+
s={s}
|
|
289
|
+
/>
|
|
290
|
+
</DetailSection>
|
|
291
|
+
|
|
292
|
+
<DetailSection
|
|
293
|
+
id="notes"
|
|
294
|
+
title="Anotações"
|
|
295
|
+
open={openSections.has("notes")}
|
|
296
|
+
onToggle={toggle}
|
|
297
|
+
s={s}
|
|
298
|
+
>
|
|
299
|
+
<p className="text-body-xs font-normal text-fg-muted">Sem anotações.</p>
|
|
300
|
+
</DetailSection>
|
|
301
|
+
</FloatingPanel>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ClientRow } from "../../_table-data";
|
|
2
|
+
|
|
3
|
+
export type DetailDrawerProps = {
|
|
4
|
+
/** Cliente em exibição. Quando `null` o drawer fica desmontado. */
|
|
5
|
+
row: ClientRow | null;
|
|
6
|
+
/** Fecha o drawer (chamado por ESC, X do header e Cancelar). */
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
/** Disparado pelo botão Editar (ícone Pencil no header). Opcional. */
|
|
9
|
+
onEdit?: (row: ClientRow) => void;
|
|
10
|
+
/** Disparado pelo botão Excluir (ícone Trash no header). Opcional. */
|
|
11
|
+
onDelete?: (row: ClientRow) => void;
|
|
12
|
+
/** Disparado pelo CTA "Salvar alterações" do footer. Opcional. */
|
|
13
|
+
onSave?: (row: ClientRow) => void;
|
|
14
|
+
};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Panel } from "@/components/ui/Panel";
|
|
3
|
+
import { Button } from "@/components/ui/Button/button";
|
|
4
|
+
import {
|
|
5
|
+
FormField,
|
|
6
|
+
FormFieldInput,
|
|
7
|
+
FormFieldSelect,
|
|
8
|
+
FormFieldTextarea,
|
|
9
|
+
} from "@/components/ui/FormField";
|
|
10
|
+
import { ChipGroup, ChipGroupItem } from "@/components/ui/Chip";
|
|
11
|
+
import { STATUSES, CATEGORIES } from "../../_table-data";
|
|
12
|
+
import { CITIES, STATUS_DOT } from "../../clientes-showcase-mocks";
|
|
13
|
+
import { drawerFormStyles } from "../../clientes-showcase.styles";
|
|
14
|
+
import type { NovoClienteFormData } from "../../clientes-showcase.types";
|
|
15
|
+
|
|
16
|
+
const INITIAL_FORM: NovoClienteFormData = {
|
|
17
|
+
name: "",
|
|
18
|
+
email: "",
|
|
19
|
+
whatsapp: "",
|
|
20
|
+
statusId: "active",
|
|
21
|
+
categoryId: "lead",
|
|
22
|
+
city: "",
|
|
23
|
+
value: 0,
|
|
24
|
+
notes: "",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type NovoClienteDrawerProps = {
|
|
28
|
+
open: boolean;
|
|
29
|
+
onOpenChange: (open: boolean) => void;
|
|
30
|
+
/** Disparado ao confirmar — drawer fecha automaticamente após o submit. */
|
|
31
|
+
onSubmit?: (data: NovoClienteFormData) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Drawer "Novo cliente" — replica visual do TblClientFormDrawer
|
|
36
|
+
* (sandbox /design-and-table-v2), recompilado em cima dos componentes do DS:
|
|
37
|
+
* Panel + FormField (Input / Select / Textarea) + ChipGroup + Button.
|
|
38
|
+
*
|
|
39
|
+
* Layout (top → bottom):
|
|
40
|
+
* 1. Nome* — full width
|
|
41
|
+
* 2. Email + WhatsApp — 2 cols
|
|
42
|
+
* 3. Status — ChipGroup com dot colorido
|
|
43
|
+
* 4. Categoria — ChipGroup
|
|
44
|
+
* 5. Cidade + Valor R$ — 2 cols
|
|
45
|
+
* 6. Notas — textarea full width
|
|
46
|
+
*/
|
|
47
|
+
export function NovoClienteDrawer({
|
|
48
|
+
open,
|
|
49
|
+
onOpenChange,
|
|
50
|
+
onSubmit,
|
|
51
|
+
}: NovoClienteDrawerProps) {
|
|
52
|
+
const s = drawerFormStyles();
|
|
53
|
+
const [form, setForm] = useState<NovoClienteFormData>(INITIAL_FORM);
|
|
54
|
+
|
|
55
|
+
// Reseta form sempre que abrir
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (open) setForm(INITIAL_FORM);
|
|
58
|
+
}, [open]);
|
|
59
|
+
|
|
60
|
+
const canSubmit = form.name.trim().length > 0;
|
|
61
|
+
|
|
62
|
+
const handleSubmit = () => {
|
|
63
|
+
if (!canSubmit) return;
|
|
64
|
+
onSubmit?.(form);
|
|
65
|
+
onOpenChange(false);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Panel
|
|
70
|
+
open={open}
|
|
71
|
+
onOpenChange={onOpenChange}
|
|
72
|
+
title="Novo cliente"
|
|
73
|
+
side="right"
|
|
74
|
+
size="md"
|
|
75
|
+
footer={
|
|
76
|
+
<>
|
|
77
|
+
<Button
|
|
78
|
+
variant="outline"
|
|
79
|
+
color="secondary"
|
|
80
|
+
size="md"
|
|
81
|
+
onClick={() => onOpenChange(false)}
|
|
82
|
+
>
|
|
83
|
+
Cancelar
|
|
84
|
+
</Button>
|
|
85
|
+
<Button
|
|
86
|
+
variant="filled"
|
|
87
|
+
color="primary"
|
|
88
|
+
size="md"
|
|
89
|
+
disabled={!canSubmit}
|
|
90
|
+
onClick={handleSubmit}
|
|
91
|
+
>
|
|
92
|
+
Criar cliente
|
|
93
|
+
</Button>
|
|
94
|
+
</>
|
|
95
|
+
}
|
|
96
|
+
>
|
|
97
|
+
<FormFieldInput
|
|
98
|
+
label="Nome"
|
|
99
|
+
required
|
|
100
|
+
placeholder="Maria Silva"
|
|
101
|
+
value={form.name}
|
|
102
|
+
onChange={(e) => setForm((f) => ({ ...f, name: e.target.value }))}
|
|
103
|
+
/>
|
|
104
|
+
|
|
105
|
+
<div className={s.twoCols()}>
|
|
106
|
+
<FormFieldInput
|
|
107
|
+
label="Email"
|
|
108
|
+
type="email"
|
|
109
|
+
placeholder="cliente@exemplo.com"
|
|
110
|
+
value={form.email}
|
|
111
|
+
onChange={(e) => setForm((f) => ({ ...f, email: e.target.value }))}
|
|
112
|
+
/>
|
|
113
|
+
<FormFieldInput
|
|
114
|
+
label="WhatsApp"
|
|
115
|
+
type="tel"
|
|
116
|
+
placeholder="55 11 99999-9999"
|
|
117
|
+
value={form.whatsapp}
|
|
118
|
+
onChange={(e) => setForm((f) => ({ ...f, whatsapp: e.target.value }))}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<FormField label="Status">
|
|
123
|
+
{() => (
|
|
124
|
+
<ChipGroup
|
|
125
|
+
type="single"
|
|
126
|
+
value={form.statusId}
|
|
127
|
+
onValueChange={(v) =>
|
|
128
|
+
v &&
|
|
129
|
+
setForm((f) => ({
|
|
130
|
+
...f,
|
|
131
|
+
statusId: v as keyof typeof STATUSES,
|
|
132
|
+
}))
|
|
133
|
+
}
|
|
134
|
+
size="md"
|
|
135
|
+
shape="pill"
|
|
136
|
+
ariaLabel="Status do cliente"
|
|
137
|
+
>
|
|
138
|
+
{Object.entries(STATUSES).map(([id, st]) => (
|
|
139
|
+
<ChipGroupItem key={id} value={id}>
|
|
140
|
+
<span
|
|
141
|
+
className={s.statusDot()}
|
|
142
|
+
style={{ background: STATUS_DOT[id] ?? st.color }}
|
|
143
|
+
aria-hidden
|
|
144
|
+
/>
|
|
145
|
+
{st.label}
|
|
146
|
+
</ChipGroupItem>
|
|
147
|
+
))}
|
|
148
|
+
</ChipGroup>
|
|
149
|
+
)}
|
|
150
|
+
</FormField>
|
|
151
|
+
|
|
152
|
+
<FormField label="Categoria">
|
|
153
|
+
{() => (
|
|
154
|
+
<ChipGroup
|
|
155
|
+
type="single"
|
|
156
|
+
value={form.categoryId}
|
|
157
|
+
onValueChange={(v) =>
|
|
158
|
+
v &&
|
|
159
|
+
setForm((f) => ({
|
|
160
|
+
...f,
|
|
161
|
+
categoryId: v as keyof typeof CATEGORIES,
|
|
162
|
+
}))
|
|
163
|
+
}
|
|
164
|
+
size="md"
|
|
165
|
+
shape="pill"
|
|
166
|
+
ariaLabel="Categoria do cliente"
|
|
167
|
+
>
|
|
168
|
+
{Object.entries(CATEGORIES).map(([id, cat]) => (
|
|
169
|
+
<ChipGroupItem key={id} value={id}>
|
|
170
|
+
{cat.label}
|
|
171
|
+
</ChipGroupItem>
|
|
172
|
+
))}
|
|
173
|
+
</ChipGroup>
|
|
174
|
+
)}
|
|
175
|
+
</FormField>
|
|
176
|
+
|
|
177
|
+
<div className={s.twoCols()}>
|
|
178
|
+
<FormFieldSelect
|
|
179
|
+
label="Cidade"
|
|
180
|
+
placeholder="Selecionar..."
|
|
181
|
+
value={form.city || undefined}
|
|
182
|
+
onValueChange={(v) => setForm((f) => ({ ...f, city: v }))}
|
|
183
|
+
options={CITIES.map((c) => ({ value: c, label: c }))}
|
|
184
|
+
/>
|
|
185
|
+
<FormFieldInput
|
|
186
|
+
label="Valor (R$)"
|
|
187
|
+
type="number"
|
|
188
|
+
startAddon="R$"
|
|
189
|
+
value={String(form.value)}
|
|
190
|
+
onChange={(e) =>
|
|
191
|
+
setForm((f) => ({ ...f, value: Number(e.target.value) }))
|
|
192
|
+
}
|
|
193
|
+
min={0}
|
|
194
|
+
step={0.01}
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<FormFieldTextarea
|
|
199
|
+
label="Notas"
|
|
200
|
+
placeholder="Observações sobre o cliente..."
|
|
201
|
+
rows={4}
|
|
202
|
+
value={form.notes}
|
|
203
|
+
onChange={(e) => setForm((f) => ({ ...f, notes: e.target.value }))}
|
|
204
|
+
/>
|
|
205
|
+
</Panel>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ClientesScreen, default } from "./clientes-screen";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { LucideIcon } from "@/lib/lucide-types";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
export type Step = {
|
|
5
|
+
id: string;
|
|
6
|
+
icon: LucideIcon;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Nav lateral (stepper) da tela de edição — itens com ícone + título +
|
|
13
|
+
* descrição; o ativo ganha destaque de marca. Clicar seleciona + rola até a
|
|
14
|
+
* seção. Composição local do showcase usando tokens DS.
|
|
15
|
+
*/
|
|
16
|
+
export function StepNav({
|
|
17
|
+
steps,
|
|
18
|
+
activeId,
|
|
19
|
+
onSelect,
|
|
20
|
+
className,
|
|
21
|
+
}: {
|
|
22
|
+
steps: Step[];
|
|
23
|
+
activeId: string;
|
|
24
|
+
onSelect: (id: string) => void;
|
|
25
|
+
className?: string;
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<nav
|
|
29
|
+
className={cn(
|
|
30
|
+
"flex flex-col gap-gp-xs rounded-radius-lg border border-border-subtle bg-bg-surface p-pad-md shadow-sh-sm",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
aria-label="Etapas do pedido"
|
|
34
|
+
>
|
|
35
|
+
{steps.map((s) => {
|
|
36
|
+
const Icon = s.icon;
|
|
37
|
+
const active = s.id === activeId;
|
|
38
|
+
return (
|
|
39
|
+
<button
|
|
40
|
+
key={s.id}
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={() => onSelect(s.id)}
|
|
43
|
+
aria-current={active ? "step" : undefined}
|
|
44
|
+
className={cn(
|
|
45
|
+
"flex items-start gap-gp-md rounded-radius-md border p-pad-lg text-left transition-colors",
|
|
46
|
+
active
|
|
47
|
+
? "border-border-brand bg-bg-brand-subtle"
|
|
48
|
+
: "border-transparent hover:bg-bg-muted",
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<span
|
|
52
|
+
className={cn(
|
|
53
|
+
"grid size-8 shrink-0 place-items-center rounded-radius-md border",
|
|
54
|
+
active
|
|
55
|
+
? "border-transparent bg-bg-brand text-fg-on-brand"
|
|
56
|
+
// bg-surface (branco no light) + borda: contrasta tanto no card
|
|
57
|
+
// quanto no hover cinza (senão o ícone funde com o hover).
|
|
58
|
+
: "border-border-subtle bg-bg-surface text-fg-muted",
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<Icon className="size-icon-sm" />
|
|
62
|
+
</span>
|
|
63
|
+
<span className="min-w-0 flex-1">
|
|
64
|
+
<span
|
|
65
|
+
className={cn(
|
|
66
|
+
"block text-body-sm font-semibold",
|
|
67
|
+
active ? "text-fg-brand" : "text-fg-default",
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{s.title}
|
|
71
|
+
</span>
|
|
72
|
+
<span className="mt-gp-2xs block text-caption-sm leading-snug text-fg-muted">
|
|
73
|
+
{s.description}
|
|
74
|
+
</span>
|
|
75
|
+
</span>
|
|
76
|
+
</button>
|
|
77
|
+
);
|
|
78
|
+
})}
|
|
79
|
+
</nav>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Card de seção flat — bg-surface + borda sutil, sem elevação pesada.
|
|
6
|
+
* Espelha o visual dos blocos da referência (Products ordered, Customer, etc).
|
|
7
|
+
* Composição local do showcase consumindo tokens DS via classes.
|
|
8
|
+
*/
|
|
9
|
+
export function SectionCard({
|
|
10
|
+
title,
|
|
11
|
+
icon,
|
|
12
|
+
action,
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
bodyClassName,
|
|
16
|
+
}: {
|
|
17
|
+
title?: ReactNode;
|
|
18
|
+
icon?: ReactNode;
|
|
19
|
+
action?: ReactNode;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
className?: string;
|
|
22
|
+
bodyClassName?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const hasHeader = !!(title || action);
|
|
25
|
+
const header = hasHeader && (
|
|
26
|
+
// Header: padding 16px (pad-2xl) + divider full-bleed embaixo.
|
|
27
|
+
<header className="flex items-center justify-between gap-gp-md border-b border-border-subtle bg-bg-subtle p-pad-2xl">
|
|
28
|
+
{/* gap ícone↔título = 12px (gap-gp-md) */}
|
|
29
|
+
<div className="flex items-center gap-gp-md">
|
|
30
|
+
{icon && (
|
|
31
|
+
<span className="grid size-8 shrink-0 place-items-center rounded-radius-md bg-bg-muted text-fg-muted">
|
|
32
|
+
{icon}
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
35
|
+
{title && (
|
|
36
|
+
<h2 className="text-title-md font-semibold text-fg-default">
|
|
37
|
+
{title}
|
|
38
|
+
</h2>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
{action}
|
|
42
|
+
</header>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section className={cn("overflow-hidden rounded-radius-lg border border-border-subtle bg-bg-surface shadow-sh-sm", className)}>
|
|
47
|
+
{header}
|
|
48
|
+
{/* body: padding 16px (pad-2xl) — uniforme; abaixo do divider quando há header */}
|
|
49
|
+
<div className={cn("p-pad-2xl", bodyClassName)}>{children}</div>
|
|
50
|
+
</section>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Divisória sutil de seção. `my` controla o respiro vertical (default md). */
|
|
55
|
+
export function SectionDivider({ className }: { className?: string }) {
|
|
56
|
+
return <div className={cn("h-px bg-border-subtle", className)} />;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Par label-uppercase + valor (grid de Customer/Shipping/Billing Information). */
|
|
60
|
+
export function Field({
|
|
61
|
+
label,
|
|
62
|
+
value,
|
|
63
|
+
accent,
|
|
64
|
+
}: {
|
|
65
|
+
label: string;
|
|
66
|
+
value: ReactNode;
|
|
67
|
+
/** Destaca o valor com a cor de marca (links, ids de transação, etc). */
|
|
68
|
+
accent?: boolean;
|
|
69
|
+
}) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="flex flex-col gap-gp-2xs">
|
|
72
|
+
<span className="text-caption-sm font-medium uppercase tracking-wider text-fg-muted">
|
|
73
|
+
{label}
|
|
74
|
+
</span>
|
|
75
|
+
<span
|
|
76
|
+
className={cn(
|
|
77
|
+
"text-body-md",
|
|
78
|
+
accent ? "text-fg-brand" : "text-fg-default",
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{value}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|