@pylonsync/create-pylon 0.3.344 → 0.3.346
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/bin/create-pylon.js +35 -0
- package/package.json +1 -1
- package/templates/crm/AGENTS.md +116 -0
- package/templates/crm/CLAUDE.md +7 -0
- package/templates/crm/README.md +96 -0
- package/templates/crm/app/companies/companies-view.tsx +129 -0
- package/templates/crm/app/companies/page.tsx +17 -0
- package/templates/crm/app/contacts/contacts-view.tsx +139 -0
- package/templates/crm/app/contacts/page.tsx +17 -0
- package/templates/crm/app/deals/[id]/deal-view.tsx +189 -0
- package/templates/crm/app/deals/[id]/page.tsx +23 -0
- package/templates/crm/app/error.tsx +43 -0
- package/templates/crm/app/globals.css +201 -0
- package/templates/crm/app/layout.tsx +31 -0
- package/templates/crm/app/login/auth-form.tsx +122 -0
- package/templates/crm/app/login/page.tsx +38 -0
- package/templates/crm/app/not-found.tsx +29 -0
- package/templates/crm/app/page.tsx +38 -0
- package/templates/crm/app/pipeline-view.tsx +99 -0
- package/templates/crm/app/robots.ts +12 -0
- package/templates/crm/app/sitemap.ts +27 -0
- package/templates/crm/app/workspace.tsx +231 -0
- package/templates/crm/app.ts +171 -0
- package/templates/crm/bunfig +4 -0
- package/templates/crm/components/activity-timeline.tsx +133 -0
- package/templates/crm/components/avatar.tsx +40 -0
- package/templates/crm/components/board-skeleton.tsx +44 -0
- package/templates/crm/components/command-palette.tsx +162 -0
- package/templates/crm/components/data-table.tsx +90 -0
- package/templates/crm/components/deal-card.tsx +91 -0
- package/templates/crm/components/deal-dialog.tsx +169 -0
- package/templates/crm/components/empty-state.tsx +43 -0
- package/templates/crm/components/kbd.tsx +23 -0
- package/templates/crm/components/metrics-bar.tsx +46 -0
- package/templates/crm/components/page-header.tsx +36 -0
- package/templates/crm/components/pipeline-board.tsx +105 -0
- package/templates/crm/components/record-dialog.tsx +126 -0
- package/templates/crm/components/sidebar.tsx +112 -0
- package/templates/crm/components/stage-badge.tsx +33 -0
- package/templates/crm/components/ui/badge.tsx +37 -0
- package/templates/crm/components/ui/button.tsx +56 -0
- package/templates/crm/components/ui/card.tsx +90 -0
- package/templates/crm/components/ui/dialog.tsx +110 -0
- package/templates/crm/components/ui/input.tsx +30 -0
- package/templates/crm/components/ui/label.tsx +21 -0
- package/templates/crm/components/ui/select.tsx +39 -0
- package/templates/crm/components/ui/table.tsx +124 -0
- package/templates/crm/components/ui/textarea.tsx +24 -0
- package/templates/crm/components.json +20 -0
- package/templates/crm/functions/createDeal.ts +59 -0
- package/templates/crm/functions/logActivity.ts +49 -0
- package/templates/crm/functions/moveDeal.ts +53 -0
- package/templates/crm/functions/seedWorkspace.ts +63 -0
- package/templates/crm/gitignore +10 -0
- package/templates/crm/lib/pipeline.ts +221 -0
- package/templates/crm/lib/search.ts +78 -0
- package/templates/crm/lib/seed.ts +130 -0
- package/templates/crm/lib/utils.ts +10 -0
- package/templates/crm/package.json +39 -0
- package/templates/crm/tests/components.test.tsx +245 -0
- package/templates/crm/tests/pipeline.test.ts +219 -0
- package/templates/crm/tests/search.test.ts +83 -0
- package/templates/crm/tests/setup.ts +5 -0
- package/templates/crm/tsconfig.json +33 -0
- package/templates/helpdesk/AGENTS.md +116 -0
- package/templates/helpdesk/CLAUDE.md +7 -0
- package/templates/helpdesk/README.md +90 -0
- package/templates/helpdesk/app/customers/customers-view.tsx +140 -0
- package/templates/helpdesk/app/customers/page.tsx +17 -0
- package/templates/helpdesk/app/error.tsx +43 -0
- package/templates/helpdesk/app/globals.css +201 -0
- package/templates/helpdesk/app/inbox-view.tsx +147 -0
- package/templates/helpdesk/app/layout.tsx +31 -0
- package/templates/helpdesk/app/login/auth-form.tsx +122 -0
- package/templates/helpdesk/app/login/page.tsx +38 -0
- package/templates/helpdesk/app/not-found.tsx +29 -0
- package/templates/helpdesk/app/page.tsx +38 -0
- package/templates/helpdesk/app/robots.ts +12 -0
- package/templates/helpdesk/app/sitemap.ts +27 -0
- package/templates/helpdesk/app/tickets/[id]/page.tsx +23 -0
- package/templates/helpdesk/app/tickets/[id]/ticket-view.tsx +164 -0
- package/templates/helpdesk/app/workspace.tsx +210 -0
- package/templates/helpdesk/app.ts +142 -0
- package/templates/helpdesk/bunfig +4 -0
- package/templates/helpdesk/components/avatar.tsx +40 -0
- package/templates/helpdesk/components/command-palette.tsx +162 -0
- package/templates/helpdesk/components/data-table.tsx +90 -0
- package/templates/helpdesk/components/empty-state.tsx +43 -0
- package/templates/helpdesk/components/kbd.tsx +23 -0
- package/templates/helpdesk/components/new-ticket-dialog.tsx +147 -0
- package/templates/helpdesk/components/page-header.tsx +36 -0
- package/templates/helpdesk/components/priority-badge.tsx +61 -0
- package/templates/helpdesk/components/record-dialog.tsx +126 -0
- package/templates/helpdesk/components/sidebar.tsx +111 -0
- package/templates/helpdesk/components/sla-indicator.tsx +47 -0
- package/templates/helpdesk/components/thread.tsx +143 -0
- package/templates/helpdesk/components/ticket-list.tsx +78 -0
- package/templates/helpdesk/components/ui/badge.tsx +37 -0
- package/templates/helpdesk/components/ui/button.tsx +56 -0
- package/templates/helpdesk/components/ui/card.tsx +90 -0
- package/templates/helpdesk/components/ui/dialog.tsx +110 -0
- package/templates/helpdesk/components/ui/input.tsx +30 -0
- package/templates/helpdesk/components/ui/label.tsx +21 -0
- package/templates/helpdesk/components/ui/select.tsx +39 -0
- package/templates/helpdesk/components/ui/table.tsx +124 -0
- package/templates/helpdesk/components/ui/textarea.tsx +24 -0
- package/templates/helpdesk/components.json +20 -0
- package/templates/helpdesk/functions/createTicket.ts +62 -0
- package/templates/helpdesk/functions/replyToTicket.ts +55 -0
- package/templates/helpdesk/functions/seedWorkspace.ts +52 -0
- package/templates/helpdesk/functions/setTicketState.ts +58 -0
- package/templates/helpdesk/gitignore +10 -0
- package/templates/helpdesk/lib/format.ts +53 -0
- package/templates/helpdesk/lib/search.ts +78 -0
- package/templates/helpdesk/lib/seed.ts +185 -0
- package/templates/helpdesk/lib/tickets.ts +184 -0
- package/templates/helpdesk/lib/utils.ts +10 -0
- package/templates/helpdesk/package.json +39 -0
- package/templates/helpdesk/tests/components.test.tsx +202 -0
- package/templates/helpdesk/tests/search.test.ts +83 -0
- package/templates/helpdesk/tests/setup.ts +5 -0
- package/templates/helpdesk/tests/tickets.test.ts +215 -0
- package/templates/helpdesk/tsconfig.json +33 -0
- package/templates/inventory/AGENTS.md +116 -0
- package/templates/inventory/CLAUDE.md +7 -0
- package/templates/inventory/README.md +103 -0
- package/templates/inventory/app/error.tsx +43 -0
- package/templates/inventory/app/globals.css +201 -0
- package/templates/inventory/app/layout.tsx +31 -0
- package/templates/inventory/app/login/auth-form.tsx +122 -0
- package/templates/inventory/app/login/page.tsx +38 -0
- package/templates/inventory/app/movements/movements-view.tsx +103 -0
- package/templates/inventory/app/movements/page.tsx +17 -0
- package/templates/inventory/app/not-found.tsx +29 -0
- package/templates/inventory/app/page.tsx +34 -0
- package/templates/inventory/app/products/[id]/page.tsx +23 -0
- package/templates/inventory/app/products/[id]/product-view.tsx +178 -0
- package/templates/inventory/app/products-view.tsx +213 -0
- package/templates/inventory/app/robots.ts +12 -0
- package/templates/inventory/app/sitemap.ts +27 -0
- package/templates/inventory/app/workspace.tsx +172 -0
- package/templates/inventory/app.ts +154 -0
- package/templates/inventory/bunfig +4 -0
- package/templates/inventory/components/avatar.tsx +40 -0
- package/templates/inventory/components/command-palette.tsx +162 -0
- package/templates/inventory/components/data-table.tsx +90 -0
- package/templates/inventory/components/empty-state.tsx +43 -0
- package/templates/inventory/components/kbd.tsx +23 -0
- package/templates/inventory/components/movement-dialog.tsx +182 -0
- package/templates/inventory/components/page-header.tsx +36 -0
- package/templates/inventory/components/record-dialog.tsx +126 -0
- package/templates/inventory/components/sidebar.tsx +111 -0
- package/templates/inventory/components/stock-level.tsx +47 -0
- package/templates/inventory/components/summary-bar.tsx +56 -0
- package/templates/inventory/components/ui/badge.tsx +37 -0
- package/templates/inventory/components/ui/button.tsx +56 -0
- package/templates/inventory/components/ui/card.tsx +90 -0
- package/templates/inventory/components/ui/dialog.tsx +110 -0
- package/templates/inventory/components/ui/input.tsx +30 -0
- package/templates/inventory/components/ui/label.tsx +21 -0
- package/templates/inventory/components/ui/select.tsx +39 -0
- package/templates/inventory/components/ui/table.tsx +124 -0
- package/templates/inventory/components/ui/textarea.tsx +24 -0
- package/templates/inventory/components.json +20 -0
- package/templates/inventory/functions/recordMovement.ts +72 -0
- package/templates/inventory/functions/seedWorkspace.ts +40 -0
- package/templates/inventory/gitignore +10 -0
- package/templates/inventory/lib/format.ts +53 -0
- package/templates/inventory/lib/search.ts +78 -0
- package/templates/inventory/lib/seed.ts +102 -0
- package/templates/inventory/lib/stock.ts +207 -0
- package/templates/inventory/lib/utils.ts +10 -0
- package/templates/inventory/package.json +39 -0
- package/templates/inventory/tests/components.test.tsx +155 -0
- package/templates/inventory/tests/search.test.ts +83 -0
- package/templates/inventory/tests/setup.ts +5 -0
- package/templates/inventory/tests/stock.test.ts +255 -0
- package/templates/inventory/tsconfig.json +33 -0
- package/templates/invoices/AGENTS.md +116 -0
- package/templates/invoices/CLAUDE.md +7 -0
- package/templates/invoices/README.md +99 -0
- package/templates/invoices/app/clients/clients-view.tsx +130 -0
- package/templates/invoices/app/clients/page.tsx +17 -0
- package/templates/invoices/app/error.tsx +43 -0
- package/templates/invoices/app/globals.css +201 -0
- package/templates/invoices/app/invoices/[id]/invoice-view.tsx +253 -0
- package/templates/invoices/app/invoices/[id]/page.tsx +23 -0
- package/templates/invoices/app/invoices-view.tsx +183 -0
- package/templates/invoices/app/layout.tsx +31 -0
- package/templates/invoices/app/login/auth-form.tsx +122 -0
- package/templates/invoices/app/login/page.tsx +38 -0
- package/templates/invoices/app/not-found.tsx +29 -0
- package/templates/invoices/app/page.tsx +36 -0
- package/templates/invoices/app/robots.ts +12 -0
- package/templates/invoices/app/sitemap.ts +27 -0
- package/templates/invoices/app/workspace.tsx +174 -0
- package/templates/invoices/app.ts +169 -0
- package/templates/invoices/bunfig +4 -0
- package/templates/invoices/components/avatar.tsx +40 -0
- package/templates/invoices/components/command-palette.tsx +162 -0
- package/templates/invoices/components/data-table.tsx +90 -0
- package/templates/invoices/components/empty-state.tsx +43 -0
- package/templates/invoices/components/kbd.tsx +23 -0
- package/templates/invoices/components/line-items.tsx +172 -0
- package/templates/invoices/components/page-header.tsx +36 -0
- package/templates/invoices/components/payment-dialog.tsx +132 -0
- package/templates/invoices/components/record-dialog.tsx +126 -0
- package/templates/invoices/components/sidebar.tsx +111 -0
- package/templates/invoices/components/status-badge.tsx +35 -0
- package/templates/invoices/components/summary-bar.tsx +65 -0
- package/templates/invoices/components/totals-panel.tsx +70 -0
- package/templates/invoices/components/ui/badge.tsx +37 -0
- package/templates/invoices/components/ui/button.tsx +56 -0
- package/templates/invoices/components/ui/card.tsx +90 -0
- package/templates/invoices/components/ui/dialog.tsx +110 -0
- package/templates/invoices/components/ui/input.tsx +30 -0
- package/templates/invoices/components/ui/label.tsx +21 -0
- package/templates/invoices/components/ui/select.tsx +39 -0
- package/templates/invoices/components/ui/table.tsx +124 -0
- package/templates/invoices/components/ui/textarea.tsx +24 -0
- package/templates/invoices/components.json +20 -0
- package/templates/invoices/functions/createInvoice.ts +49 -0
- package/templates/invoices/functions/recordPayment.ts +72 -0
- package/templates/invoices/functions/seedWorkspace.ts +57 -0
- package/templates/invoices/functions/setInvoiceStatus.ts +35 -0
- package/templates/invoices/gitignore +10 -0
- package/templates/invoices/lib/billing.ts +259 -0
- package/templates/invoices/lib/format.ts +53 -0
- package/templates/invoices/lib/search.ts +78 -0
- package/templates/invoices/lib/seed.ts +190 -0
- package/templates/invoices/lib/utils.ts +10 -0
- package/templates/invoices/package.json +39 -0
- package/templates/invoices/tests/billing.test.ts +283 -0
- package/templates/invoices/tests/components.test.tsx +172 -0
- package/templates/invoices/tests/search.test.ts +83 -0
- package/templates/invoices/tests/setup.ts +5 -0
- package/templates/invoices/tsconfig.json +33 -0
- package/templates/projects/AGENTS.md +116 -0
- package/templates/projects/CLAUDE.md +7 -0
- package/templates/projects/README.md +96 -0
- package/templates/projects/app/clients/clients-view.tsx +126 -0
- package/templates/projects/app/clients/page.tsx +17 -0
- package/templates/projects/app/error.tsx +43 -0
- package/templates/projects/app/globals.css +201 -0
- package/templates/projects/app/layout.tsx +31 -0
- package/templates/projects/app/login/auth-form.tsx +122 -0
- package/templates/projects/app/login/page.tsx +38 -0
- package/templates/projects/app/not-found.tsx +29 -0
- package/templates/projects/app/page.tsx +30 -0
- package/templates/projects/app/projects/[id]/page.tsx +23 -0
- package/templates/projects/app/projects/[id]/project-view.tsx +179 -0
- package/templates/projects/app/projects-view.tsx +181 -0
- package/templates/projects/app/robots.ts +12 -0
- package/templates/projects/app/sitemap.ts +27 -0
- package/templates/projects/app/workspace.tsx +190 -0
- package/templates/projects/app.ts +186 -0
- package/templates/projects/bunfig +4 -0
- package/templates/projects/components/avatar.tsx +40 -0
- package/templates/projects/components/board-skeleton.tsx +37 -0
- package/templates/projects/components/budget-bar.tsx +61 -0
- package/templates/projects/components/command-palette.tsx +162 -0
- package/templates/projects/components/data-table.tsx +90 -0
- package/templates/projects/components/empty-state.tsx +43 -0
- package/templates/projects/components/kbd.tsx +23 -0
- package/templates/projects/components/page-header.tsx +36 -0
- package/templates/projects/components/record-dialog.tsx +126 -0
- package/templates/projects/components/sidebar.tsx +111 -0
- package/templates/projects/components/task-board.tsx +109 -0
- package/templates/projects/components/task-card.tsx +77 -0
- package/templates/projects/components/time-dialog.tsx +114 -0
- package/templates/projects/components/ui/badge.tsx +37 -0
- package/templates/projects/components/ui/button.tsx +56 -0
- package/templates/projects/components/ui/card.tsx +90 -0
- package/templates/projects/components/ui/dialog.tsx +110 -0
- package/templates/projects/components/ui/input.tsx +30 -0
- package/templates/projects/components/ui/label.tsx +21 -0
- package/templates/projects/components/ui/select.tsx +39 -0
- package/templates/projects/components/ui/table.tsx +124 -0
- package/templates/projects/components/ui/textarea.tsx +24 -0
- package/templates/projects/components.json +20 -0
- package/templates/projects/functions/logTime.ts +49 -0
- package/templates/projects/functions/moveTask.ts +40 -0
- package/templates/projects/functions/seedWorkspace.ts +59 -0
- package/templates/projects/gitignore +10 -0
- package/templates/projects/lib/format.ts +53 -0
- package/templates/projects/lib/search.ts +78 -0
- package/templates/projects/lib/seed.ts +140 -0
- package/templates/projects/lib/utils.ts +10 -0
- package/templates/projects/lib/work.ts +233 -0
- package/templates/projects/package.json +39 -0
- package/templates/projects/tests/components.test.tsx +200 -0
- package/templates/projects/tests/search.test.ts +83 -0
- package/templates/projects/tests/setup.ts +5 -0
- package/templates/projects/tests/work.test.ts +238 -0
- package/templates/projects/tsconfig.json +33 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { callFn, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { Inbox, Plus } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Select } from "@/components/ui/select";
|
|
8
|
+
import { Kbd } from "@/components/kbd";
|
|
9
|
+
import { PageHeader } from "@/components/page-header";
|
|
10
|
+
import { EmptyState } from "@/components/empty-state";
|
|
11
|
+
import { TicketList } from "@/components/ticket-list";
|
|
12
|
+
import { NewTicketDialog } from "@/components/new-ticket-dialog";
|
|
13
|
+
import { STATUSES, applyFilter, counts, type Filter } from "@/lib/tickets";
|
|
14
|
+
import { Workspace } from "./workspace";
|
|
15
|
+
|
|
16
|
+
export function InboxView({
|
|
17
|
+
email,
|
|
18
|
+
openNew,
|
|
19
|
+
initialFilter,
|
|
20
|
+
}: {
|
|
21
|
+
email: string;
|
|
22
|
+
openNew?: boolean;
|
|
23
|
+
initialFilter?: string;
|
|
24
|
+
}) {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
const [dialogOpen, setDialogOpen] = useState(Boolean(openNew));
|
|
27
|
+
const [status, setStatus] = useState<string>("open");
|
|
28
|
+
const [unassigned, setUnassigned] = useState(initialFilter === "unassigned");
|
|
29
|
+
|
|
30
|
+
// "c" opens a ticket, the way an issue tracker does. Ignored while typing so
|
|
31
|
+
// it never swallows a character in the reply box.
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
function onKey(event: KeyboardEvent) {
|
|
34
|
+
const target = event.target as HTMLElement | null;
|
|
35
|
+
const typing =
|
|
36
|
+
!!target &&
|
|
37
|
+
(target.tagName === "INPUT" ||
|
|
38
|
+
target.tagName === "TEXTAREA" ||
|
|
39
|
+
target.isContentEditable);
|
|
40
|
+
if (event.key === "c" && !typing && !event.metaKey && !event.ctrlKey) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
setDialogOpen(true);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
window.addEventListener("keydown", onKey);
|
|
46
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Workspace email={email} pathname="/">
|
|
51
|
+
{(data) => {
|
|
52
|
+
const filter: Filter = {
|
|
53
|
+
...(status === "all" ? {} : { status }),
|
|
54
|
+
...(unassigned ? { unassigned: true } : {}),
|
|
55
|
+
};
|
|
56
|
+
const visible = applyFilter(data.tickets, filter);
|
|
57
|
+
const queue = counts(data.tickets);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<PageHeader title="Inbox" count={visible.length}>
|
|
62
|
+
<div className="w-32">
|
|
63
|
+
<Select
|
|
64
|
+
aria-label="Status"
|
|
65
|
+
value={status}
|
|
66
|
+
onChange={(event) => setStatus(event.target.value)}
|
|
67
|
+
className="h-7 text-[12px]"
|
|
68
|
+
>
|
|
69
|
+
<option value="all">All statuses</option>
|
|
70
|
+
{STATUSES.map((s) => (
|
|
71
|
+
<option key={s.id} value={s.id}>
|
|
72
|
+
{s.label}
|
|
73
|
+
</option>
|
|
74
|
+
))}
|
|
75
|
+
</Select>
|
|
76
|
+
</div>
|
|
77
|
+
<Button
|
|
78
|
+
size="sm"
|
|
79
|
+
variant={unassigned ? "default" : "secondary"}
|
|
80
|
+
onClick={() => setUnassigned((on) => !on)}
|
|
81
|
+
title="Show only tickets with no agent"
|
|
82
|
+
>
|
|
83
|
+
Unassigned
|
|
84
|
+
<span className="tabular ml-1 opacity-70">{queue.unassigned}</span>
|
|
85
|
+
</Button>
|
|
86
|
+
<Button size="sm" onClick={() => setDialogOpen(true)}>
|
|
87
|
+
<Plus />
|
|
88
|
+
New ticket
|
|
89
|
+
<Kbd className="ml-1 border-primary-foreground/25 bg-primary-foreground/10 text-primary-foreground/70">
|
|
90
|
+
c
|
|
91
|
+
</Kbd>
|
|
92
|
+
</Button>
|
|
93
|
+
</PageHeader>
|
|
94
|
+
|
|
95
|
+
{visible.length === 0 ? (
|
|
96
|
+
<EmptyState
|
|
97
|
+
icon={<Inbox />}
|
|
98
|
+
title={
|
|
99
|
+
data.loading
|
|
100
|
+
? "Loading…"
|
|
101
|
+
: data.tickets.length === 0
|
|
102
|
+
? "No tickets yet"
|
|
103
|
+
: "Nothing matches this filter"
|
|
104
|
+
}
|
|
105
|
+
description={
|
|
106
|
+
data.tickets.length === 0
|
|
107
|
+
? "When a customer writes in, their ticket lands here. You can also open one yourself after a phone call."
|
|
108
|
+
: "Try a different status, or clear the unassigned filter."
|
|
109
|
+
}
|
|
110
|
+
action={
|
|
111
|
+
data.tickets.length === 0 ? (
|
|
112
|
+
<Button size="sm" onClick={() => setDialogOpen(true)}>
|
|
113
|
+
<Plus />
|
|
114
|
+
New ticket
|
|
115
|
+
</Button>
|
|
116
|
+
) : undefined
|
|
117
|
+
}
|
|
118
|
+
/>
|
|
119
|
+
) : (
|
|
120
|
+
<TicketList
|
|
121
|
+
tickets={visible}
|
|
122
|
+
customerName={data.customerName}
|
|
123
|
+
assigneeName={data.agentName}
|
|
124
|
+
onSelect={(id) => router.push(`/tickets/${id}`)}
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
|
|
128
|
+
<NewTicketDialog
|
|
129
|
+
open={dialogOpen}
|
|
130
|
+
customers={data.customers}
|
|
131
|
+
onOpenChange={setDialogOpen}
|
|
132
|
+
onCreate={async (draft) => {
|
|
133
|
+
const result = await callFn<{ id: string }>("createTicket", {
|
|
134
|
+
subject: draft.subject,
|
|
135
|
+
body: draft.body,
|
|
136
|
+
priority: draft.priority,
|
|
137
|
+
...(draft.customerId ? { customerId: draft.customerId } : {}),
|
|
138
|
+
});
|
|
139
|
+
if (result?.id) router.push(`/tickets/${result.id}`);
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
}}
|
|
145
|
+
</Workspace>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
interface LayoutProps {
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The document shell. The app chrome — sidebar, command palette, shortcuts —
|
|
9
|
+
* lives in `app/workspace.tsx` instead, because it needs the synced data; this
|
|
10
|
+
* layer only sets up the page.
|
|
11
|
+
*
|
|
12
|
+
* `class="dark"` makes dark the default theme. The light tokens in globals.css
|
|
13
|
+
* are complete and tested, so removing the class flips the whole app; swap it
|
|
14
|
+
* for a stored preference when you want a toggle.
|
|
15
|
+
*/
|
|
16
|
+
export default function RootLayout({ children }: LayoutProps) {
|
|
17
|
+
return (
|
|
18
|
+
<html lang="en" className="dark">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charSet="utf-8" />
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
22
|
+
<title>__APP_NAME__</title>
|
|
23
|
+
{/* Tailwind is compiled by Pylon from app/globals.css and the
|
|
24
|
+
stylesheet link is injected here automatically. Inter is declared in
|
|
25
|
+
app.ts (fonts: [...]) and self-hosted by the build — @font-face, the
|
|
26
|
+
preload link, and a size-adjusted fallback are injected too. */}
|
|
27
|
+
</head>
|
|
28
|
+
<body className="h-full">{children}</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ApiError,
|
|
6
|
+
passwordLogin,
|
|
7
|
+
passwordRegister,
|
|
8
|
+
persistSession,
|
|
9
|
+
} from "@pylonsync/client";
|
|
10
|
+
import { Button } from "@/components/ui/button";
|
|
11
|
+
import { Input } from "@/components/ui/input";
|
|
12
|
+
import { Label } from "@/components/ui/label";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* One form, two modes. Calls the built-in auth API (`/api/auth/password/*`),
|
|
16
|
+
* then `persistSession` writes the token so the sync engine and `callFn`
|
|
17
|
+
* authenticate as this user on the next load.
|
|
18
|
+
*
|
|
19
|
+
* The navigation is a full `assign`, not a client route change, so the SSR
|
|
20
|
+
* runtime re-resolves auth from the HttpOnly cookie and the first paint of the
|
|
21
|
+
* workspace is already signed in.
|
|
22
|
+
*/
|
|
23
|
+
export function AuthForm() {
|
|
24
|
+
const [mode, setMode] = useState<"login" | "signup">("login");
|
|
25
|
+
const [email, setEmail] = useState("");
|
|
26
|
+
const [password, setPassword] = useState("");
|
|
27
|
+
const [error, setError] = useState<string | null>(null);
|
|
28
|
+
const [pending, setPending] = useState(false);
|
|
29
|
+
|
|
30
|
+
async function onSubmit(event: React.FormEvent) {
|
|
31
|
+
event.preventDefault();
|
|
32
|
+
setError(null);
|
|
33
|
+
setPending(true);
|
|
34
|
+
try {
|
|
35
|
+
const session =
|
|
36
|
+
mode === "login"
|
|
37
|
+
? await passwordLogin({ email, password })
|
|
38
|
+
: await passwordRegister({ email, password });
|
|
39
|
+
persistSession(session);
|
|
40
|
+
window.location.assign("/");
|
|
41
|
+
} catch (err) {
|
|
42
|
+
setError(messageFor(err));
|
|
43
|
+
setPending(false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className="space-y-5">
|
|
49
|
+
<form onSubmit={onSubmit} className="space-y-3.5">
|
|
50
|
+
<div className="space-y-1.5">
|
|
51
|
+
<Label htmlFor="email">Email</Label>
|
|
52
|
+
<Input
|
|
53
|
+
id="email"
|
|
54
|
+
type="email"
|
|
55
|
+
value={email}
|
|
56
|
+
onChange={(event) => setEmail(event.target.value)}
|
|
57
|
+
required
|
|
58
|
+
autoComplete="email"
|
|
59
|
+
placeholder="you@company.com"
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
<div className="space-y-1.5">
|
|
63
|
+
<Label htmlFor="password">Password</Label>
|
|
64
|
+
<Input
|
|
65
|
+
id="password"
|
|
66
|
+
type="password"
|
|
67
|
+
value={password}
|
|
68
|
+
onChange={(event) => setPassword(event.target.value)}
|
|
69
|
+
required
|
|
70
|
+
autoComplete={mode === "login" ? "current-password" : "new-password"}
|
|
71
|
+
placeholder={mode === "login" ? "Your password" : "At least 10 characters"}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
{error ? (
|
|
75
|
+
<p className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-[12px] leading-snug text-destructive">
|
|
76
|
+
{error}
|
|
77
|
+
</p>
|
|
78
|
+
) : null}
|
|
79
|
+
<Button type="submit" disabled={pending} className="w-full">
|
|
80
|
+
{pending ? "…" : mode === "login" ? "Sign in" : "Create account"}
|
|
81
|
+
</Button>
|
|
82
|
+
</form>
|
|
83
|
+
|
|
84
|
+
<p className="text-center text-[12px] text-muted-foreground">
|
|
85
|
+
{mode === "login" ? "First time here?" : "Already have an account?"}{" "}
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
onClick={() => {
|
|
89
|
+
setMode(mode === "login" ? "signup" : "login");
|
|
90
|
+
setError(null);
|
|
91
|
+
}}
|
|
92
|
+
className="font-medium text-foreground underline underline-offset-2"
|
|
93
|
+
>
|
|
94
|
+
{mode === "login" ? "Create an account" : "Sign in"}
|
|
95
|
+
</button>
|
|
96
|
+
</p>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// `ApiError` carries a stable `.code`, so branch on the code rather than the
|
|
102
|
+
// message — the copy here is ours, the message is the framework's.
|
|
103
|
+
function messageFor(err: unknown): string {
|
|
104
|
+
if (err instanceof ApiError) {
|
|
105
|
+
switch (err.code) {
|
|
106
|
+
case "INVALID_CREDENTIALS":
|
|
107
|
+
return "Wrong email or password.";
|
|
108
|
+
case "USER_EXISTS":
|
|
109
|
+
return "That email is already registered — sign in instead.";
|
|
110
|
+
case "WEAK_PASSWORD":
|
|
111
|
+
return "Pick a longer password — at least 10 characters.";
|
|
112
|
+
case "PWNED_PASSWORD":
|
|
113
|
+
return "That password has appeared in a known breach. Choose another.";
|
|
114
|
+
case "RATE_LIMITED":
|
|
115
|
+
return "Too many attempts — try again in a minute.";
|
|
116
|
+
default:
|
|
117
|
+
return err.message;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (err instanceof Error) return err.message;
|
|
121
|
+
return "Something went wrong. Try again.";
|
|
122
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { AuthForm } from "./auth-form";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Sign in",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function LoginPage({ auth, response }: PageProps) {
|
|
11
|
+
// Already signed in? Nothing to do here.
|
|
12
|
+
if (auth.user_id && !auth.user_id.startsWith("guest_")) {
|
|
13
|
+
response.redirect("/");
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<main className="flex min-h-screen items-center justify-center px-6">
|
|
19
|
+
<div className="w-full max-w-[320px]">
|
|
20
|
+
<div className="mb-7 flex items-center gap-2">
|
|
21
|
+
<span className="flex size-7 items-center justify-center rounded-md bg-primary text-[12px] font-bold text-primary-foreground">
|
|
22
|
+
C
|
|
23
|
+
</span>
|
|
24
|
+
<span className="text-[15px] font-semibold tracking-tight">CRM</span>
|
|
25
|
+
</div>
|
|
26
|
+
<h1 className="text-[19px] font-semibold tracking-tight">
|
|
27
|
+
Sign in to your workspace
|
|
28
|
+
</h1>
|
|
29
|
+
<p className="mt-1 text-[13px] text-muted-foreground">
|
|
30
|
+
Your team shares one pipeline. Anyone with an account sees it.
|
|
31
|
+
</p>
|
|
32
|
+
<div className="mt-6">
|
|
33
|
+
<AuthForm />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</main>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Link, useRouter, type NotFoundProps } from "@pylonsync/react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
|
|
5
|
+
// `app/not-found.tsx` → rendered at HTTP 404 for any unmatched URL (and when
|
|
6
|
+
// a page calls `response.notFound()`). It's HYDRATED, so it's interactive:
|
|
7
|
+
// the buttons below use the client router. Not-found boundaries receive the
|
|
8
|
+
// standard page props (and, matching Next, no `reset`).
|
|
9
|
+
export default function NotFound(_props: NotFoundProps) {
|
|
10
|
+
const router = useRouter();
|
|
11
|
+
return (
|
|
12
|
+
<div className="space-y-6">
|
|
13
|
+
<section>
|
|
14
|
+
<h1 className="text-2xl font-semibold tracking-tight">404</h1>
|
|
15
|
+
<p className="mt-2 text-muted-foreground">
|
|
16
|
+
We couldn't find that page.
|
|
17
|
+
</p>
|
|
18
|
+
</section>
|
|
19
|
+
<div className="flex items-center gap-3">
|
|
20
|
+
<Button onClick={() => router.back()} variant="outline">
|
|
21
|
+
← Go back
|
|
22
|
+
</Button>
|
|
23
|
+
<Button asChild>
|
|
24
|
+
<Link href="/">Home</Link>
|
|
25
|
+
</Button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { InboxView } from "./inbox-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Inbox",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `/` — the support queue.
|
|
12
|
+
*
|
|
13
|
+
* Server-side auth gate only. Reading `auth` opts this render out of caching,
|
|
14
|
+
* which is correct: every screen here is private.
|
|
15
|
+
*
|
|
16
|
+
* The signed-in email comes from the User row, not from `auth` — `PageAuth` is
|
|
17
|
+
* `{ user_id, is_admin, tenant_id, roles }` and carries no email.
|
|
18
|
+
*/
|
|
19
|
+
export default function InboxPage({
|
|
20
|
+
auth,
|
|
21
|
+
response,
|
|
22
|
+
searchParams,
|
|
23
|
+
serverData,
|
|
24
|
+
}: PageProps) {
|
|
25
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
26
|
+
response.redirect("/login");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<InboxView
|
|
33
|
+
email={me?.email ?? ""}
|
|
34
|
+
openNew={searchParams?.new === "ticket"}
|
|
35
|
+
initialFilter={searchParams?.filter}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Robots } from "@pylonsync/react";
|
|
2
|
+
|
|
3
|
+
// app/robots.ts → served at /robots.txt. The default export may also be async.
|
|
4
|
+
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
5
|
+
|
|
6
|
+
export default function robots(): Robots {
|
|
7
|
+
return {
|
|
8
|
+
// Keep the authenticated app and the API out of the index.
|
|
9
|
+
rules: { userAgent: "*", allow: "/", disallow: ["/dashboard", "/api/"] },
|
|
10
|
+
sitemap: `${SITE}/sitemap.xml`,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Sitemap } from "@pylonsync/react";
|
|
2
|
+
|
|
3
|
+
// app/sitemap.ts → served at /sitemap.xml. The default export can be async, so
|
|
4
|
+
// it can enumerate dynamic pages from your database. Point SITE_URL at your
|
|
5
|
+
// domain in production.
|
|
6
|
+
const SITE = process.env.SITE_URL ?? "http://localhost:4321";
|
|
7
|
+
|
|
8
|
+
export default async function sitemap(): Promise<Sitemap> {
|
|
9
|
+
// Only public pages belong here — /dashboard is private (and noindex), so
|
|
10
|
+
// it's intentionally left out.
|
|
11
|
+
const staticRoutes: Sitemap = [
|
|
12
|
+
{ url: `${SITE}/`, changeFrequency: "weekly", priority: 1 },
|
|
13
|
+
{ url: `${SITE}/login`, changeFrequency: "yearly", priority: 0.3 },
|
|
14
|
+
{ url: `${SITE}/signup`, changeFrequency: "yearly", priority: 0.5 },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
// The export is async, so you can enumerate dynamic pages from a DB read:
|
|
18
|
+
//
|
|
19
|
+
// const posts = await fetchPublishedPosts();
|
|
20
|
+
// const postRoutes: Sitemap = posts.map((p) => ({
|
|
21
|
+
// url: `${SITE}/blog/${p.slug}`,
|
|
22
|
+
// lastModified: p.updatedAt,
|
|
23
|
+
// }));
|
|
24
|
+
// return [...staticRoutes, ...postRoutes];
|
|
25
|
+
|
|
26
|
+
return staticRoutes;
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React, { use } from "react";
|
|
2
|
+
import type { Metadata, PageProps } from "@pylonsync/react";
|
|
3
|
+
import { TicketView } from "./ticket-view";
|
|
4
|
+
|
|
5
|
+
export const metadata: Metadata = {
|
|
6
|
+
title: "Ticket",
|
|
7
|
+
robots: "noindex",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** `app/tickets/[id]/page.tsx` -> `/tickets/:id`. */
|
|
11
|
+
export default function TicketPage({
|
|
12
|
+
auth,
|
|
13
|
+
response,
|
|
14
|
+
params,
|
|
15
|
+
serverData,
|
|
16
|
+
}: PageProps<{ id: string }>) {
|
|
17
|
+
if (!auth.user_id || auth.user_id.startsWith("guest_")) {
|
|
18
|
+
response.redirect("/login");
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const me = use(serverData.get<{ email?: string }>("User", auth.user_id));
|
|
22
|
+
return <TicketView email={me?.email ?? ""} ticketId={params.id} />;
|
|
23
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Link, callFn, useRouter } from "@pylonsync/react";
|
|
5
|
+
import { ArrowLeft } from "lucide-react";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { Select } from "@/components/ui/select";
|
|
8
|
+
import { PageHeader } from "@/components/page-header";
|
|
9
|
+
import { EmptyState } from "@/components/empty-state";
|
|
10
|
+
import { Thread } from "@/components/thread";
|
|
11
|
+
import { PriorityBadge } from "@/components/priority-badge";
|
|
12
|
+
import { SlaIndicator } from "@/components/sla-indicator";
|
|
13
|
+
import { Avatar } from "@/components/avatar";
|
|
14
|
+
import { relativeTime } from "@/lib/format";
|
|
15
|
+
import { PRIORITIES, STATUSES, ticketNumber } from "@/lib/tickets";
|
|
16
|
+
import { Workspace } from "../../workspace";
|
|
17
|
+
|
|
18
|
+
export function TicketView({
|
|
19
|
+
email,
|
|
20
|
+
ticketId,
|
|
21
|
+
}: {
|
|
22
|
+
email: string;
|
|
23
|
+
ticketId: string;
|
|
24
|
+
}) {
|
|
25
|
+
const router = useRouter();
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Workspace email={email} pathname="/">
|
|
29
|
+
{(data) => {
|
|
30
|
+
const ticket = data.tickets.find((t) => t.id === ticketId);
|
|
31
|
+
|
|
32
|
+
if (!ticket) {
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<PageHeader title="Ticket" />
|
|
36
|
+
<EmptyState
|
|
37
|
+
title={data.loading ? "Loading…" : "Ticket not found"}
|
|
38
|
+
description={
|
|
39
|
+
data.loading
|
|
40
|
+
? undefined
|
|
41
|
+
: "It may have been deleted, or the link is wrong."
|
|
42
|
+
}
|
|
43
|
+
action={
|
|
44
|
+
data.loading ? undefined : (
|
|
45
|
+
<Button size="sm" variant="secondary" asChild>
|
|
46
|
+
<Link href="/">Back to inbox</Link>
|
|
47
|
+
</Button>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
/>
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const customer = data.customers.find((c) => c.id === ticket.customerId);
|
|
56
|
+
const messages = data.messages.filter((m) => m.ticketId === ticket.id);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<PageHeader title={ticket.subject}>
|
|
61
|
+
<SlaIndicator ticket={ticket} />
|
|
62
|
+
<div className="w-28">
|
|
63
|
+
<Select
|
|
64
|
+
aria-label="Priority"
|
|
65
|
+
value={ticket.priority}
|
|
66
|
+
className="h-7 text-[12px]"
|
|
67
|
+
onChange={(event) =>
|
|
68
|
+
void callFn("setTicketState", {
|
|
69
|
+
ticketId: ticket.id,
|
|
70
|
+
priority: event.target.value,
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
{PRIORITIES.map((p) => (
|
|
75
|
+
<option key={p.id} value={p.id}>
|
|
76
|
+
{p.label}
|
|
77
|
+
</option>
|
|
78
|
+
))}
|
|
79
|
+
</Select>
|
|
80
|
+
</div>
|
|
81
|
+
<div className="w-28">
|
|
82
|
+
<Select
|
|
83
|
+
aria-label="Status"
|
|
84
|
+
value={ticket.status}
|
|
85
|
+
className="h-7 text-[12px]"
|
|
86
|
+
onChange={(event) =>
|
|
87
|
+
void callFn("setTicketState", {
|
|
88
|
+
ticketId: ticket.id,
|
|
89
|
+
status: event.target.value,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
>
|
|
93
|
+
{STATUSES.map((s) => (
|
|
94
|
+
<option key={s.id} value={s.id}>
|
|
95
|
+
{s.label}
|
|
96
|
+
</option>
|
|
97
|
+
))}
|
|
98
|
+
</Select>
|
|
99
|
+
</div>
|
|
100
|
+
</PageHeader>
|
|
101
|
+
|
|
102
|
+
<div className="flex items-center gap-3 border-b border-border px-6 py-2.5 text-[12px] text-muted-foreground">
|
|
103
|
+
<Link
|
|
104
|
+
href="/"
|
|
105
|
+
className="inline-flex items-center gap-1.5 transition-colors hover:text-foreground"
|
|
106
|
+
>
|
|
107
|
+
<ArrowLeft className="size-3.5" />
|
|
108
|
+
Inbox
|
|
109
|
+
</Link>
|
|
110
|
+
<span aria-hidden="true">·</span>
|
|
111
|
+
<span className="tabular">
|
|
112
|
+
{ticketNumber(ticket.id, ticket.createdAt)}
|
|
113
|
+
</span>
|
|
114
|
+
<span aria-hidden="true">·</span>
|
|
115
|
+
<PriorityBadge priority={ticket.priority} />
|
|
116
|
+
{customer ? (
|
|
117
|
+
<>
|
|
118
|
+
<span aria-hidden="true">·</span>
|
|
119
|
+
<span className="flex items-center gap-1.5">
|
|
120
|
+
<Avatar name={customer.name} size="sm" />
|
|
121
|
+
{customer.name}
|
|
122
|
+
{customer.company ? ` · ${customer.company}` : ""}
|
|
123
|
+
</span>
|
|
124
|
+
</>
|
|
125
|
+
) : null}
|
|
126
|
+
<span aria-hidden="true">·</span>
|
|
127
|
+
<span>Opened {relativeTime(ticket.createdAt)}</span>
|
|
128
|
+
|
|
129
|
+
<div className="ml-auto w-40">
|
|
130
|
+
<Select
|
|
131
|
+
aria-label="Assignee"
|
|
132
|
+
value={ticket.assigneeId ?? ""}
|
|
133
|
+
className="h-7 text-[12px]"
|
|
134
|
+
onChange={(event) =>
|
|
135
|
+
void callFn("setTicketState", {
|
|
136
|
+
ticketId: ticket.id,
|
|
137
|
+
assigneeId: event.target.value,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
>
|
|
141
|
+
<option value="">Unassigned</option>
|
|
142
|
+
{data.agents.map((agent) => (
|
|
143
|
+
<option key={agent.id} value={agent.id}>
|
|
144
|
+
{agent.displayName || agent.email}
|
|
145
|
+
</option>
|
|
146
|
+
))}
|
|
147
|
+
</Select>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<Thread
|
|
152
|
+
messages={messages}
|
|
153
|
+
authorName={data.agentName}
|
|
154
|
+
customerName={customer?.name ?? null}
|
|
155
|
+
onSend={(body, internal) =>
|
|
156
|
+
callFn("replyToTicket", { ticketId: ticket.id, body, internal })
|
|
157
|
+
}
|
|
158
|
+
/>
|
|
159
|
+
</>
|
|
160
|
+
);
|
|
161
|
+
}}
|
|
162
|
+
</Workspace>
|
|
163
|
+
);
|
|
164
|
+
}
|