@viliha/vui-ui 1.1.8 → 1.2.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/AGENT.md +79 -9
- package/README.md +60 -0
- package/bin/vui.mjs +211 -0
- package/package.json +10 -2
- package/template/.env.example +20 -0
- package/template/app/(app)/branches/branches-table.tsx +69 -0
- package/template/app/(app)/branches/page.tsx +13 -0
- package/template/app/(app)/businesses/businesses-table.tsx +39 -0
- package/template/app/(app)/businesses/page.tsx +13 -0
- package/template/app/(app)/calendar/page.tsx +725 -0
- package/template/app/(app)/charts/charts-content.tsx +190 -0
- package/template/app/(app)/charts/layout.tsx +8 -0
- package/template/app/(app)/charts/page.tsx +44 -0
- package/template/app/(app)/chat/layout.tsx +14 -0
- package/template/app/(app)/chat/page.tsx +311 -0
- package/template/app/(app)/components/layout.tsx +8 -0
- package/template/app/(app)/components/page.tsx +464 -0
- package/template/app/(app)/crm/companies/companies-table.tsx +56 -0
- package/template/app/(app)/crm/companies/page.tsx +13 -0
- package/template/app/(app)/crm/opportunities/opportunities-board.tsx +484 -0
- package/template/app/(app)/crm/opportunities/page.tsx +13 -0
- package/template/app/(app)/crm/people/page.tsx +13 -0
- package/template/app/(app)/crm/people/people-table.tsx +50 -0
- package/template/app/(app)/dashboard/layout.tsx +8 -0
- package/template/app/(app)/dashboard/page.tsx +239 -0
- package/template/app/(app)/departments/departments-table.tsx +55 -0
- package/template/app/(app)/departments/page.tsx +13 -0
- package/template/app/(app)/employees/employees-table.tsx +64 -0
- package/template/app/(app)/employees/page.tsx +13 -0
- package/template/app/(app)/forms/layout.tsx +8 -0
- package/template/app/(app)/forms/page.tsx +388 -0
- package/template/app/(app)/layout.tsx +74 -0
- package/template/app/(app)/markets/markets-table.tsx +76 -0
- package/template/app/(app)/markets/page.tsx +13 -0
- package/template/app/(app)/organizations/edit/page.tsx +72 -0
- package/template/app/(app)/organizations/new/page.tsx +46 -0
- package/template/app/(app)/organizations/organizations-config.tsx +97 -0
- package/template/app/(app)/organizations/organizations-table.tsx +42 -0
- package/template/app/(app)/organizations/page.tsx +13 -0
- package/template/app/(app)/settings/layout.tsx +8 -0
- package/template/app/(app)/settings/page.tsx +255 -0
- package/template/app/(app)/steps/page.tsx +263 -0
- package/template/app/(app)/support/layout.tsx +14 -0
- package/template/app/(app)/support/page.tsx +345 -0
- package/template/app/(app)/system/cities/cities-table.tsx +34 -0
- package/template/app/(app)/system/cities/page.tsx +13 -0
- package/template/app/(app)/system/countries/countries-table.tsx +34 -0
- package/template/app/(app)/system/countries/page.tsx +13 -0
- package/template/app/(app)/system/currencies/currencies-table.tsx +34 -0
- package/template/app/(app)/system/currencies/page.tsx +13 -0
- package/template/app/(app)/system/languages/languages-table.tsx +32 -0
- package/template/app/(app)/system/languages/page.tsx +13 -0
- package/template/app/(app)/system/regions/page.tsx +13 -0
- package/template/app/(app)/system/regions/regions-table.tsx +32 -0
- package/template/app/(app)/users/page.tsx +13 -0
- package/template/app/(app)/users/users-table.tsx +97 -0
- package/template/app/_components/app-sidebar.tsx +565 -0
- package/template/app/_components/auth.tsx +184 -0
- package/template/app/_components/breadcrumbs.tsx +26 -0
- package/template/app/_components/global-search.tsx +209 -0
- package/template/app/_components/logo.tsx +49 -0
- package/template/app/_components/nav-config.ts +138 -0
- package/template/app/_components/open-tabs.tsx +530 -0
- package/template/app/_components/quick-actions.tsx +140 -0
- package/template/app/_components/route-meta.ts +137 -0
- package/template/app/_components/set-page-title.tsx +15 -0
- package/template/app/_components/showcase.tsx +29 -0
- package/template/app/_components/stat-card.tsx +56 -0
- package/template/app/_components/theme-toggle.tsx +46 -0
- package/template/app/_components/top-bar.tsx +109 -0
- package/template/app/_components/user-menu.tsx +109 -0
- package/template/app/_components/wordmark.tsx +45 -0
- package/template/app/apple-icon.png +0 -0
- package/template/app/auth/forgot-password/page.tsx +102 -0
- package/template/app/auth/layout.tsx +43 -0
- package/template/app/auth/page.tsx +13 -0
- package/template/app/auth/reset-password/page.tsx +108 -0
- package/template/app/auth/signin/page.tsx +238 -0
- package/template/app/auth/signup/page.tsx +159 -0
- package/template/app/auth/verify/page.tsx +86 -0
- package/template/app/error.tsx +56 -0
- package/template/app/globals.css +6 -0
- package/template/app/icon.png +0 -0
- package/template/app/icon.svg +4 -0
- package/template/app/layout.tsx +103 -0
- package/template/app/not-found.tsx +29 -0
- package/template/app/onboarding/layout.tsx +31 -0
- package/template/app/onboarding/page.tsx +399 -0
- package/template/app/page.tsx +13 -0
- package/template/app/register-business/layout.tsx +30 -0
- package/template/app/register-business/page.tsx +234 -0
- package/template/components/ui/accordion.tsx +66 -0
- package/template/components/ui/alert-dialog.tsx +196 -0
- package/template/components/ui/alert.tsx +66 -0
- package/template/components/ui/aspect-ratio.tsx +11 -0
- package/template/components/ui/avatar.tsx +109 -0
- package/template/components/ui/badge.tsx +48 -0
- package/template/components/ui/breadcrumb.tsx +109 -0
- package/template/components/ui/button.tsx +64 -0
- package/template/components/ui/calendar.tsx +220 -0
- package/template/components/ui/card.tsx +92 -0
- package/template/components/ui/checkbox.tsx +32 -0
- package/template/components/ui/collapsible.tsx +33 -0
- package/template/components/ui/command.tsx +184 -0
- package/template/components/ui/dialog.tsx +158 -0
- package/template/components/ui/dropdown-menu.tsx +257 -0
- package/template/components/ui/form.tsx +167 -0
- package/template/components/ui/hover-card.tsx +44 -0
- package/template/components/ui/input-otp.tsx +77 -0
- package/template/components/ui/input.tsx +21 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/popover.tsx +89 -0
- package/template/components/ui/progress.tsx +31 -0
- package/template/components/ui/radio-group.tsx +45 -0
- package/template/components/ui/scroll-area.tsx +58 -0
- package/template/components/ui/select.tsx +190 -0
- package/template/components/ui/separator.tsx +28 -0
- package/template/components/ui/sheet.tsx +143 -0
- package/template/components/ui/skeleton.tsx +13 -0
- package/template/components/ui/slider.tsx +63 -0
- package/template/components/ui/sonner.tsx +40 -0
- package/template/components/ui/switch.tsx +35 -0
- package/template/components/ui/table.tsx +116 -0
- package/template/components/ui/tabs.tsx +91 -0
- package/template/components/ui/textarea.tsx +18 -0
- package/template/components/ui/toggle-group.tsx +83 -0
- package/template/components/ui/toggle.tsx +47 -0
- package/template/components/ui/tooltip.tsx +57 -0
- package/template/lib/auth-demo.ts +94 -0
- package/template/lib/crm-data.ts +80 -0
- package/template/lib/demo-data.ts +267 -0
- package/template/lib/mock-data.ts +169 -0
- package/template/lib/org-store.ts +50 -0
- package/template/lib/seo.ts +188 -0
- package/template/lib/utils.ts +3 -0
- package/template/next.config.mjs +13 -0
- package/template/postcss.config.mjs +7 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { format } from "date-fns";
|
|
5
|
+
import { PaperPlaneIcon, QuestionMarkCircledIcon } from "@radix-ui/react-icons";
|
|
6
|
+
|
|
7
|
+
import { cn } from "@viliha/vui-ui/utils";
|
|
8
|
+
import { Button } from "@viliha/vui-ui/button";
|
|
9
|
+
import { Input } from "@viliha/vui-ui/input";
|
|
10
|
+
import { Select } from "@viliha/vui-ui/select";
|
|
11
|
+
import { Breadcrumbs } from "@/app/_components/breadcrumbs";
|
|
12
|
+
import { SetPageTitle } from "@/app/_components/set-page-title";
|
|
13
|
+
|
|
14
|
+
type Status = "open" | "pending" | "resolved";
|
|
15
|
+
type Priority = "low" | "medium" | "high" | "urgent";
|
|
16
|
+
type Comment = {
|
|
17
|
+
id: number;
|
|
18
|
+
author: string;
|
|
19
|
+
role: "agent" | "customer";
|
|
20
|
+
text: string;
|
|
21
|
+
time: string;
|
|
22
|
+
};
|
|
23
|
+
type Ticket = {
|
|
24
|
+
id: number;
|
|
25
|
+
ref: string;
|
|
26
|
+
subject: string;
|
|
27
|
+
requester: string;
|
|
28
|
+
assignee: string;
|
|
29
|
+
status: Status;
|
|
30
|
+
priority: Priority;
|
|
31
|
+
updated: string;
|
|
32
|
+
description: string;
|
|
33
|
+
comments: Comment[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Status + priority colors — static Tailwind (content colors, not chrome).
|
|
37
|
+
const STATUS_BADGE: Record<Status, string> = {
|
|
38
|
+
open: "bg-blue-100 text-blue-700 dark:bg-blue-500/15 dark:text-blue-300",
|
|
39
|
+
pending: "bg-amber-100 text-amber-700 dark:bg-amber-500/15 dark:text-amber-300",
|
|
40
|
+
resolved: "bg-emerald-100 text-emerald-700 dark:bg-emerald-500/15 dark:text-emerald-300",
|
|
41
|
+
};
|
|
42
|
+
const PRIORITY_DOT: Record<Priority, string> = {
|
|
43
|
+
low: "bg-slate-400",
|
|
44
|
+
medium: "bg-sky-500",
|
|
45
|
+
high: "bg-orange-500",
|
|
46
|
+
urgent: "bg-red-500",
|
|
47
|
+
};
|
|
48
|
+
const STATUS_OPTIONS = [
|
|
49
|
+
{ value: "open", label: "Open" },
|
|
50
|
+
{ value: "pending", label: "Pending" },
|
|
51
|
+
{ value: "resolved", label: "Resolved" },
|
|
52
|
+
];
|
|
53
|
+
const PRIORITY_OPTIONS = [
|
|
54
|
+
{ value: "low", label: "Low" },
|
|
55
|
+
{ value: "medium", label: "Medium" },
|
|
56
|
+
{ value: "high", label: "High" },
|
|
57
|
+
{ value: "urgent", label: "Urgent" },
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
const FIRST_TICKET: Ticket = {
|
|
61
|
+
id: 1,
|
|
62
|
+
ref: "TCK-1042",
|
|
63
|
+
subject: "Invoice not received for June",
|
|
64
|
+
requester: "Ava Chen",
|
|
65
|
+
assignee: "You",
|
|
66
|
+
status: "open",
|
|
67
|
+
priority: "high",
|
|
68
|
+
updated: "10 min ago",
|
|
69
|
+
description:
|
|
70
|
+
"Hi, I haven't received my June invoice yet. My billing email is ava@acme.co. Can you resend it and confirm the amount?",
|
|
71
|
+
comments: [
|
|
72
|
+
{ id: 1, author: "You", role: "agent", text: "Hi Ava — checking now, I'll resend within the hour.", time: "9:20 AM" },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const SEED: Ticket[] = [
|
|
77
|
+
FIRST_TICKET,
|
|
78
|
+
{
|
|
79
|
+
id: 2,
|
|
80
|
+
ref: "TCK-1041",
|
|
81
|
+
subject: "Cannot reset my password",
|
|
82
|
+
requester: "Marcus Reed",
|
|
83
|
+
assignee: "Unassigned",
|
|
84
|
+
status: "pending",
|
|
85
|
+
priority: "urgent",
|
|
86
|
+
updated: "1 hr ago",
|
|
87
|
+
description: "The reset link says expired every time I click it, even immediately after requesting a new one.",
|
|
88
|
+
comments: [],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 3,
|
|
92
|
+
ref: "TCK-1038",
|
|
93
|
+
subject: "Feature request: CSV export",
|
|
94
|
+
requester: "Priya Nair",
|
|
95
|
+
assignee: "You",
|
|
96
|
+
status: "resolved",
|
|
97
|
+
priority: "low",
|
|
98
|
+
updated: "Yesterday",
|
|
99
|
+
description: "Would love a CSV export on the reports page for offline analysis.",
|
|
100
|
+
comments: [
|
|
101
|
+
{ id: 1, author: "You", role: "agent", text: "Shipped in 1.1.7 🎉", time: "Yesterday" },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
export default function SupportPage() {
|
|
107
|
+
const [tickets, setTickets] = React.useState<Ticket[]>(SEED);
|
|
108
|
+
const [activeId, setActiveId] = React.useState(FIRST_TICKET.id);
|
|
109
|
+
const [query, setQuery] = React.useState("");
|
|
110
|
+
const [statusFilter, setStatusFilter] = React.useState("all");
|
|
111
|
+
const [draft, setDraft] = React.useState("");
|
|
112
|
+
|
|
113
|
+
const active = tickets.find((t) => t.id === activeId) ?? FIRST_TICKET;
|
|
114
|
+
const q = query.trim().toLowerCase();
|
|
115
|
+
const filtered = tickets.filter(
|
|
116
|
+
(t) =>
|
|
117
|
+
(statusFilter === "all" || t.status === statusFilter) &&
|
|
118
|
+
(t.subject.toLowerCase().includes(q) ||
|
|
119
|
+
t.ref.toLowerCase().includes(q) ||
|
|
120
|
+
t.requester.toLowerCase().includes(q)),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const patch = (data: Partial<Ticket>) =>
|
|
124
|
+
setTickets((prev) => prev.map((t) => (t.id === activeId ? { ...t, ...data } : t)));
|
|
125
|
+
|
|
126
|
+
const addReply = () => {
|
|
127
|
+
const text = draft.trim();
|
|
128
|
+
if (!text) return;
|
|
129
|
+
setTickets((prev) =>
|
|
130
|
+
prev.map((t) =>
|
|
131
|
+
t.id === activeId
|
|
132
|
+
? {
|
|
133
|
+
...t,
|
|
134
|
+
status: "pending",
|
|
135
|
+
updated: "just now",
|
|
136
|
+
comments: [
|
|
137
|
+
...t.comments,
|
|
138
|
+
{ id: Date.now(), author: "You", role: "agent", text, time: format(new Date(), "h:mm a") },
|
|
139
|
+
],
|
|
140
|
+
}
|
|
141
|
+
: t,
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
setDraft("");
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<div className="flex h-full flex-col">
|
|
149
|
+
<SetPageTitle title="Support" icon={QuestionMarkCircledIcon} />
|
|
150
|
+
<div className="flex h-12 shrink-0 items-center border-b border-border px-4">
|
|
151
|
+
<Breadcrumbs />
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="flex min-h-0 flex-1 p-4">
|
|
155
|
+
<div className="flex min-h-0 flex-1 overflow-hidden rounded-lg border border-border bg-card">
|
|
156
|
+
{/* Ticket queue */}
|
|
157
|
+
<aside className="flex w-80 shrink-0 flex-col border-r border-border">
|
|
158
|
+
<div className="flex shrink-0 flex-col gap-2 border-b border-border p-3">
|
|
159
|
+
<Input
|
|
160
|
+
value={query}
|
|
161
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
162
|
+
placeholder="Search tickets…"
|
|
163
|
+
aria-label="Search tickets"
|
|
164
|
+
/>
|
|
165
|
+
<Select
|
|
166
|
+
value={statusFilter}
|
|
167
|
+
onValueChange={setStatusFilter}
|
|
168
|
+
options={[{ value: "all", label: "All statuses" }, ...STATUS_OPTIONS]}
|
|
169
|
+
ariaLabel="Filter by status"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="min-h-0 flex-1 overflow-y-auto">
|
|
173
|
+
{filtered.map((t) => (
|
|
174
|
+
<button
|
|
175
|
+
key={t.id}
|
|
176
|
+
type="button"
|
|
177
|
+
onClick={() => setActiveId(t.id)}
|
|
178
|
+
className={cn(
|
|
179
|
+
"flex w-full flex-col gap-1 border-b border-border px-3 py-2.5 text-left transition-colors hover:bg-accent/40",
|
|
180
|
+
t.id === activeId && "bg-accent/60",
|
|
181
|
+
)}
|
|
182
|
+
>
|
|
183
|
+
<span className="flex items-center justify-between gap-2">
|
|
184
|
+
<span className="truncate text-sm font-medium">{t.subject}</span>
|
|
185
|
+
<StatusBadge status={t.status} />
|
|
186
|
+
</span>
|
|
187
|
+
<span className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
188
|
+
<span className={cn("size-2 shrink-0 rounded-full", PRIORITY_DOT[t.priority])} />
|
|
189
|
+
<span className="tabular-nums">{t.ref}</span>
|
|
190
|
+
<span>·</span>
|
|
191
|
+
<span className="truncate">{t.requester}</span>
|
|
192
|
+
<span className="ml-auto shrink-0">{t.updated}</span>
|
|
193
|
+
</span>
|
|
194
|
+
</button>
|
|
195
|
+
))}
|
|
196
|
+
{filtered.length === 0 && (
|
|
197
|
+
<p className="p-4 text-center text-sm text-muted-foreground">
|
|
198
|
+
No tickets found.
|
|
199
|
+
</p>
|
|
200
|
+
)}
|
|
201
|
+
</div>
|
|
202
|
+
</aside>
|
|
203
|
+
|
|
204
|
+
{/* Ticket detail */}
|
|
205
|
+
<section className="flex min-w-0 flex-1 flex-col">
|
|
206
|
+
<header className="shrink-0 border-b border-border px-5 py-3">
|
|
207
|
+
<div className="flex items-center gap-2">
|
|
208
|
+
<StatusBadge status={active.status} />
|
|
209
|
+
<span className="text-xs tabular-nums text-muted-foreground">
|
|
210
|
+
{active.ref}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
<h2 className="mt-1 truncate text-base font-semibold">{active.subject}</h2>
|
|
214
|
+
</header>
|
|
215
|
+
|
|
216
|
+
<div className="flex min-h-0 flex-1">
|
|
217
|
+
{/* Conversation + activity */}
|
|
218
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
219
|
+
<div className="min-h-0 flex-1 space-y-4 overflow-y-auto p-5">
|
|
220
|
+
{/* Original request */}
|
|
221
|
+
<Entry
|
|
222
|
+
author={active.requester}
|
|
223
|
+
kind="customer"
|
|
224
|
+
time="Opened this ticket"
|
|
225
|
+
text={active.description}
|
|
226
|
+
/>
|
|
227
|
+
{active.comments.map((c) => (
|
|
228
|
+
<Entry key={c.id} author={c.author} kind={c.role} time={c.time} text={c.text} />
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
{/* Reply */}
|
|
232
|
+
<div className="shrink-0 border-t border-border p-4">
|
|
233
|
+
<textarea
|
|
234
|
+
value={draft}
|
|
235
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
236
|
+
placeholder="Write a reply…"
|
|
237
|
+
aria-label="Reply"
|
|
238
|
+
rows={3}
|
|
239
|
+
className="w-full resize-y rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
240
|
+
/>
|
|
241
|
+
<div className="mt-2 flex justify-end">
|
|
242
|
+
<Button variant="primary" onClick={addReply} disabled={!draft.trim()}>
|
|
243
|
+
<PaperPlaneIcon className="size-4" />
|
|
244
|
+
Send reply
|
|
245
|
+
</Button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
{/* Properties */}
|
|
251
|
+
<aside className="hidden w-60 shrink-0 flex-col gap-4 border-l border-border p-4 lg:flex">
|
|
252
|
+
<Prop label="Status">
|
|
253
|
+
<Select
|
|
254
|
+
value={active.status}
|
|
255
|
+
onValueChange={(v) => patch({ status: v as Status })}
|
|
256
|
+
options={STATUS_OPTIONS}
|
|
257
|
+
ariaLabel="Status"
|
|
258
|
+
/>
|
|
259
|
+
</Prop>
|
|
260
|
+
<Prop label="Priority">
|
|
261
|
+
<Select
|
|
262
|
+
value={active.priority}
|
|
263
|
+
onValueChange={(v) => patch({ priority: v as Priority })}
|
|
264
|
+
options={PRIORITY_OPTIONS}
|
|
265
|
+
ariaLabel="Priority"
|
|
266
|
+
/>
|
|
267
|
+
</Prop>
|
|
268
|
+
<Prop label="Requester">
|
|
269
|
+
<p className="text-sm">{active.requester}</p>
|
|
270
|
+
</Prop>
|
|
271
|
+
<Prop label="Assignee">
|
|
272
|
+
<p className="text-sm">{active.assignee}</p>
|
|
273
|
+
</Prop>
|
|
274
|
+
<Prop label="Last updated">
|
|
275
|
+
<p className="text-sm text-muted-foreground">{active.updated}</p>
|
|
276
|
+
</Prop>
|
|
277
|
+
</aside>
|
|
278
|
+
</div>
|
|
279
|
+
</section>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const initials = (name: string) =>
|
|
287
|
+
name.split(" ").map((p) => p[0]).slice(0, 2).join("").toUpperCase();
|
|
288
|
+
|
|
289
|
+
function Entry({
|
|
290
|
+
author,
|
|
291
|
+
kind,
|
|
292
|
+
time,
|
|
293
|
+
text,
|
|
294
|
+
}: {
|
|
295
|
+
author: string;
|
|
296
|
+
kind: "agent" | "customer";
|
|
297
|
+
time: string;
|
|
298
|
+
text: string;
|
|
299
|
+
}) {
|
|
300
|
+
return (
|
|
301
|
+
<div className="flex gap-3">
|
|
302
|
+
<span
|
|
303
|
+
className={cn(
|
|
304
|
+
"grid size-8 shrink-0 place-items-center rounded-full text-xs font-semibold text-white",
|
|
305
|
+
kind === "agent" ? "bg-[var(--button-primary)]" : "bg-slate-500",
|
|
306
|
+
)}
|
|
307
|
+
>
|
|
308
|
+
{initials(author)}
|
|
309
|
+
</span>
|
|
310
|
+
<div className="min-w-0 flex-1 rounded-lg border border-border bg-background p-3">
|
|
311
|
+
<div className="mb-1 flex items-center gap-2">
|
|
312
|
+
<span className="text-sm font-medium">{author}</span>
|
|
313
|
+
<span className="text-xs text-muted-foreground">
|
|
314
|
+
{kind === "agent" ? "Agent" : "Customer"} · {time}
|
|
315
|
+
</span>
|
|
316
|
+
</div>
|
|
317
|
+
<p className="whitespace-pre-wrap text-sm leading-relaxed">{text}</p>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function Prop({ label, children }: { label: string; children: React.ReactNode }) {
|
|
324
|
+
return (
|
|
325
|
+
<div className="space-y-1.5">
|
|
326
|
+
<div className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
327
|
+
{label}
|
|
328
|
+
</div>
|
|
329
|
+
{children}
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function StatusBadge({ status }: { status: Status }) {
|
|
335
|
+
return (
|
|
336
|
+
<span
|
|
337
|
+
className={cn(
|
|
338
|
+
"shrink-0 rounded-full px-2 py-0.5 text-[11px] font-medium capitalize",
|
|
339
|
+
STATUS_BADGE[status],
|
|
340
|
+
)}
|
|
341
|
+
>
|
|
342
|
+
{status}
|
|
343
|
+
</span>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BookmarkIcon as Flag,
|
|
5
|
+
HomeIcon as Landmark,
|
|
6
|
+
SewingPinFilledIcon as MapPin,
|
|
7
|
+
} from "@radix-ui/react-icons";
|
|
8
|
+
|
|
9
|
+
import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
|
|
10
|
+
import { cities, type City } from "@/lib/mock-data";
|
|
11
|
+
|
|
12
|
+
const fields: RecordField<City>[] = [
|
|
13
|
+
{ key: "name", label: "Name", editable: true, required: true, group: "General", hideInTable: true },
|
|
14
|
+
{ key: "state", label: "State", icon: MapPin, editable: true, group: "General" },
|
|
15
|
+
{ key: "country", label: "Country", icon: Flag, editable: true, group: "General" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function CitiesTable() {
|
|
19
|
+
return (
|
|
20
|
+
<RecordView
|
|
21
|
+
title="Cities"
|
|
22
|
+
singular="City"
|
|
23
|
+
icon={Landmark}
|
|
24
|
+
fields={fields}
|
|
25
|
+
initialData={cities}
|
|
26
|
+
getPrimary={(row) => ({
|
|
27
|
+
title: row.name,
|
|
28
|
+
subtitle: row.country,
|
|
29
|
+
initials: row.name.slice(0, 2).toUpperCase(),
|
|
30
|
+
})}
|
|
31
|
+
makeEmptyRow={() => ({ id: Date.now(), name: "", state: "", country: "" })}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { pageMeta } from "@/lib/seo";
|
|
2
|
+
|
|
3
|
+
import { CitiesTable } from "./cities-table";
|
|
4
|
+
|
|
5
|
+
export const metadata = pageMeta("/system/cities");
|
|
6
|
+
|
|
7
|
+
export default function CitiesPage() {
|
|
8
|
+
return (
|
|
9
|
+
<main className="h-full">
|
|
10
|
+
<CitiesTable />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BookmarkIcon as Flag,
|
|
5
|
+
CodeIcon as Hash,
|
|
6
|
+
GlobeIcon as Globe,
|
|
7
|
+
} from "@radix-ui/react-icons";
|
|
8
|
+
|
|
9
|
+
import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
|
|
10
|
+
import { countries, type Country } from "@/lib/mock-data";
|
|
11
|
+
|
|
12
|
+
const fields: RecordField<Country>[] = [
|
|
13
|
+
{ key: "name", label: "Name", editable: true, required: true, group: "General", hideInTable: true },
|
|
14
|
+
{ key: "code", label: "Code", icon: Hash, editable: true, group: "General" },
|
|
15
|
+
{ key: "region", label: "Region", icon: Globe, editable: true, width: 220, group: "General" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function CountriesTable() {
|
|
19
|
+
return (
|
|
20
|
+
<RecordView
|
|
21
|
+
title="Countries"
|
|
22
|
+
singular="Country"
|
|
23
|
+
icon={Flag}
|
|
24
|
+
fields={fields}
|
|
25
|
+
initialData={countries}
|
|
26
|
+
getPrimary={(row) => ({
|
|
27
|
+
title: row.name,
|
|
28
|
+
subtitle: row.code,
|
|
29
|
+
initials: row.code.slice(0, 2).toUpperCase(),
|
|
30
|
+
})}
|
|
31
|
+
makeEmptyRow={() => ({ id: Date.now(), name: "", code: "", region: "" })}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { pageMeta } from "@/lib/seo";
|
|
2
|
+
|
|
3
|
+
import { CountriesTable } from "./countries-table";
|
|
4
|
+
|
|
5
|
+
export const metadata = pageMeta("/system/countries");
|
|
6
|
+
|
|
7
|
+
export default function CountriesPage() {
|
|
8
|
+
return (
|
|
9
|
+
<main className="h-full">
|
|
10
|
+
<CountriesTable />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CodeIcon as Hash,
|
|
5
|
+
TokensIcon as Banknote,
|
|
6
|
+
TokensIcon as Coins,
|
|
7
|
+
} from "@radix-ui/react-icons";
|
|
8
|
+
|
|
9
|
+
import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
|
|
10
|
+
import { currencies, type Currency } from "@/lib/mock-data";
|
|
11
|
+
|
|
12
|
+
const fields: RecordField<Currency>[] = [
|
|
13
|
+
{ key: "name", label: "Name", editable: true, required: true, group: "General", hideInTable: true },
|
|
14
|
+
{ key: "code", label: "Code", icon: Hash, editable: true, group: "General" },
|
|
15
|
+
{ key: "symbol", label: "Symbol", icon: Banknote, editable: true, group: "General" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function CurrenciesTable() {
|
|
19
|
+
return (
|
|
20
|
+
<RecordView
|
|
21
|
+
title="Currencies"
|
|
22
|
+
singular="Currency"
|
|
23
|
+
icon={Coins}
|
|
24
|
+
fields={fields}
|
|
25
|
+
initialData={currencies}
|
|
26
|
+
getPrimary={(row) => ({
|
|
27
|
+
title: row.name,
|
|
28
|
+
subtitle: row.code,
|
|
29
|
+
initials: row.code.slice(0, 2).toUpperCase(),
|
|
30
|
+
})}
|
|
31
|
+
makeEmptyRow={() => ({ id: Date.now(), name: "", code: "", symbol: "" })}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { pageMeta } from "@/lib/seo";
|
|
2
|
+
|
|
3
|
+
import { CurrenciesTable } from "./currencies-table";
|
|
4
|
+
|
|
5
|
+
export const metadata = pageMeta("/system/currencies");
|
|
6
|
+
|
|
7
|
+
export default function CurrenciesPage() {
|
|
8
|
+
return (
|
|
9
|
+
<main className="h-full">
|
|
10
|
+
<CurrenciesTable />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CodeIcon as Hash,
|
|
5
|
+
TextIcon as LanguagesIcon,
|
|
6
|
+
} from "@radix-ui/react-icons";
|
|
7
|
+
|
|
8
|
+
import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
|
|
9
|
+
import { languages, type Language } from "@/lib/mock-data";
|
|
10
|
+
|
|
11
|
+
const fields: RecordField<Language>[] = [
|
|
12
|
+
{ key: "name", label: "Name", editable: true, required: true, group: "General", hideInTable: true },
|
|
13
|
+
{ key: "code", label: "Code", icon: Hash, editable: true, group: "General" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function LanguagesTable() {
|
|
17
|
+
return (
|
|
18
|
+
<RecordView
|
|
19
|
+
title="Languages"
|
|
20
|
+
singular="Language"
|
|
21
|
+
icon={LanguagesIcon}
|
|
22
|
+
fields={fields}
|
|
23
|
+
initialData={languages}
|
|
24
|
+
getPrimary={(row) => ({
|
|
25
|
+
title: row.name,
|
|
26
|
+
subtitle: row.code,
|
|
27
|
+
initials: row.code.slice(0, 2).toUpperCase(),
|
|
28
|
+
})}
|
|
29
|
+
makeEmptyRow={() => ({ id: Date.now(), name: "", code: "" })}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { pageMeta } from "@/lib/seo";
|
|
2
|
+
|
|
3
|
+
import { LanguagesTable } from "./languages-table";
|
|
4
|
+
|
|
5
|
+
export const metadata = pageMeta("/system/languages");
|
|
6
|
+
|
|
7
|
+
export default function LanguagesPage() {
|
|
8
|
+
return (
|
|
9
|
+
<main className="h-full">
|
|
10
|
+
<LanguagesTable />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { pageMeta } from "@/lib/seo";
|
|
2
|
+
|
|
3
|
+
import { RegionsTable } from "./regions-table";
|
|
4
|
+
|
|
5
|
+
export const metadata = pageMeta("/system/regions");
|
|
6
|
+
|
|
7
|
+
export default function RegionsPage() {
|
|
8
|
+
return (
|
|
9
|
+
<main className="h-full">
|
|
10
|
+
<RegionsTable />
|
|
11
|
+
</main>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CodeIcon as Hash,
|
|
5
|
+
GlobeIcon as Globe,
|
|
6
|
+
} from "@radix-ui/react-icons";
|
|
7
|
+
|
|
8
|
+
import { RecordView, type RecordField } from "@viliha/vui-ui/record-view";
|
|
9
|
+
import { regions, type Region } from "@/lib/mock-data";
|
|
10
|
+
|
|
11
|
+
const fields: RecordField<Region>[] = [
|
|
12
|
+
{ key: "name", label: "Name", editable: true, required: true, group: "General", hideInTable: true },
|
|
13
|
+
{ key: "code", label: "Code", icon: Hash, editable: true, group: "General" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function RegionsTable() {
|
|
17
|
+
return (
|
|
18
|
+
<RecordView
|
|
19
|
+
title="Regions"
|
|
20
|
+
singular="Region"
|
|
21
|
+
icon={Globe}
|
|
22
|
+
fields={fields}
|
|
23
|
+
initialData={regions}
|
|
24
|
+
getPrimary={(row) => ({
|
|
25
|
+
title: row.name,
|
|
26
|
+
subtitle: row.code,
|
|
27
|
+
initials: row.code.slice(0, 2).toUpperCase(),
|
|
28
|
+
})}
|
|
29
|
+
makeEmptyRow={() => ({ id: Date.now(), name: "", code: "" })}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|