@actioneer/ads 1.0.8 → 1.0.9
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/DESIGN.md +42 -8
- package/README.md +16 -4
- package/components/assistant-ui/composer-harness.js +5 -4
- package/components/breadcrumbs.js +12 -12
- package/components/button.d.ts +16 -1
- package/components/button.js +7 -4
- package/components/chart.d.ts +31 -4
- package/components/chart.js +747 -572
- package/components/chip.d.ts +2 -1
- package/components/chip.js +37 -34
- package/components/filters.d.ts +37 -0
- package/components/filters.js +129 -0
- package/components/index.d.ts +1 -0
- package/components/index.js +447 -444
- package/components/modal.d.ts +2 -1
- package/components/modal.js +101 -90
- package/components/popover.d.ts +6 -1
- package/components/popover.js +74 -64
- package/components/sidebar.d.ts +15 -0
- package/components/sidebar.js +186 -149
- package/components/table.d.ts +8 -0
- package/components/table.js +357 -350
- package/components/tabs.d.ts +6 -1
- package/components/tabs.js +28 -31
- package/demo/components/add-to-artifact.js +3 -2
- package/examples/README.md +3 -2
- package/examples/campaign-analysis-screen.tsx +138 -0
- package/examples/campaigns-screen.tsx +439 -0
- package/lib/icons.js +3 -3
- package/llms.txt +81 -17
- package/manifest.json +1601 -83
- package/package.json +1 -1
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaigns screen — the ADS collection-screen recipe, built only from ADS exports.
|
|
3
|
+
*
|
|
4
|
+
* Follows principles `collection-screens`, `record-detail-panels`,
|
|
5
|
+
* `status-language` and `tabs-search-opposed` (see `get_principles`):
|
|
6
|
+
*
|
|
7
|
+
* - One white panel: BodyPageHeader (title + one primary action), then the
|
|
8
|
+
* control row — Tabs with counts leading, SearchDisclosure then Filters as
|
|
9
|
+
* icon triggers trailing — then the Table.
|
|
10
|
+
* - The rows fill the panel: the page size is derived from the height that is
|
|
11
|
+
* left, so Pagination always sits on the panel's bottom edge.
|
|
12
|
+
* - Row checkboxes appear on hover (the header checkbox is always visible).
|
|
13
|
+
* - Clicking a row opens its detail panel beside the list and marks the row
|
|
14
|
+
* with `isActive` — no radio, no modal.
|
|
15
|
+
* - The detail panel: kicker, title, status Chip + context under the title,
|
|
16
|
+
* then Performance (stat cards + chart card) and Details (label–value rows).
|
|
17
|
+
* - New campaign opens as a Modal over the list (never a separate screen): one
|
|
18
|
+
* scrolling form — Campaign name, Caller numbers, Audience — with Cancel and
|
|
19
|
+
* Create campaign together in the footer.
|
|
20
|
+
*
|
|
21
|
+
* In a full app this screen sits inside the shell, next to the Sidebar
|
|
22
|
+
* (put the customer's logo in its brand slot with SidebarWorkspace).
|
|
23
|
+
* Copy it and replace CAMPAIGNS with your own records.
|
|
24
|
+
*/
|
|
25
|
+
import * as React from "react";
|
|
26
|
+
import {
|
|
27
|
+
Avatar,
|
|
28
|
+
BodyPageHeader,
|
|
29
|
+
Button,
|
|
30
|
+
Checkbox,
|
|
31
|
+
Chip,
|
|
32
|
+
Drawer,
|
|
33
|
+
DrawerBody,
|
|
34
|
+
DrawerFooter,
|
|
35
|
+
DrawerHeader,
|
|
36
|
+
DrawerTitle,
|
|
37
|
+
FileDropzone,
|
|
38
|
+
Filters,
|
|
39
|
+
InputField,
|
|
40
|
+
LineChart,
|
|
41
|
+
Modal,
|
|
42
|
+
ModalBody,
|
|
43
|
+
ModalFooter,
|
|
44
|
+
ModalHeader,
|
|
45
|
+
ModalTitle,
|
|
46
|
+
Pagination,
|
|
47
|
+
SearchDisclosure,
|
|
48
|
+
SegmentedControl,
|
|
49
|
+
Tab,
|
|
50
|
+
TabList,
|
|
51
|
+
Table,
|
|
52
|
+
TableBody,
|
|
53
|
+
TableCell,
|
|
54
|
+
TableColumn,
|
|
55
|
+
TableHeader,
|
|
56
|
+
TableRow,
|
|
57
|
+
Tabs,
|
|
58
|
+
type FilterGroup,
|
|
59
|
+
} from "@actioneer/ads";
|
|
60
|
+
|
|
61
|
+
/** Business-language statuses, one tone per meaning (principle `status-language`). */
|
|
62
|
+
export type CampaignStatus = "Active" | "Scheduled" | "Paused" | "Completed" | "Draft" | "Failed";
|
|
63
|
+
|
|
64
|
+
export const STATUS_COLOR: Record<CampaignStatus, "info" | "warning" | "success" | "default" | "danger"> = {
|
|
65
|
+
Active: "info",
|
|
66
|
+
Scheduled: "info",
|
|
67
|
+
Paused: "warning",
|
|
68
|
+
Completed: "success",
|
|
69
|
+
Draft: "default",
|
|
70
|
+
Failed: "danger",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export interface CampaignRecord {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
status: CampaignStatus;
|
|
77
|
+
leads: number;
|
|
78
|
+
connectRate: string;
|
|
79
|
+
booked: number;
|
|
80
|
+
owner: string;
|
|
81
|
+
started: string;
|
|
82
|
+
context: string;
|
|
83
|
+
callingWindows: number;
|
|
84
|
+
/** Connected calls for the last seven days. */
|
|
85
|
+
week: number[];
|
|
86
|
+
cohorts?: CampaignRecord[];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const week = (...values: number[]) => values;
|
|
90
|
+
|
|
91
|
+
export const CAMPAIGNS: CampaignRecord[] = [
|
|
92
|
+
{
|
|
93
|
+
id: "q4", name: "Q4 renewal reminders", status: "Active", leads: 2400, connectRate: "44%", booked: 286, owner: "Divya Shetty", started: "Oct 22", context: "Running since Oct 22", callingWindows: 2, week: week(210, 260, 240, 310, 290, 180, 150),
|
|
94
|
+
cohorts: [
|
|
95
|
+
{ id: "q4-blr", name: "Bengaluru cohort", status: "Active", leads: 1500, connectRate: "47%", booked: 188, owner: "Divya Shetty", started: "Oct 22", context: "Running since Oct 22", callingWindows: 2, week: week(140, 170, 160, 210, 190, 120, 100) },
|
|
96
|
+
{ id: "q4-bom", name: "Mumbai cohort", status: "Paused", leads: 900, connectRate: "39%", booked: 98, owner: "Kiran Menon", started: "Oct 22", context: "Paused today at 11:40", callingWindows: 2, week: week(70, 90, 80, 100, 100, 60, 0) },
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
{ id: "emi", name: "Loan EMI nudges", status: "Completed", leads: 5120, connectRate: "41%", booked: 612, owner: "Vikram Nair", started: "Oct 14", context: "Finished Oct 21", callingWindows: 3, week: week(420, 510, 480, 620, 580, 310, 260) },
|
|
100
|
+
{ id: "kyc", name: "KYC re-verification", status: "Active", leads: 3260, connectRate: "38%", booked: 402, owner: "Rohan Iyer", started: "Oct 20", context: "Running since Oct 20", callingWindows: 2, week: week(260, 300, 320, 340, 330, 280, 310) },
|
|
101
|
+
{ id: "lapse", name: "Policy lapse win-back", status: "Draft", leads: 780, connectRate: "—", booked: 0, owner: "Asha Rao", started: "—", context: "Not launched", callingWindows: 1, week: week(0, 0, 0, 0, 0, 0, 0) },
|
|
102
|
+
{ id: "card", name: "Card activation follow-up", status: "Scheduled", leads: 1840, connectRate: "—", booked: 0, owner: "Meera Pillai", started: "Oct 28", context: "Starts Oct 28 at 10:00", callingWindows: 2, week: week(0, 0, 0, 0, 0, 0, 0) },
|
|
103
|
+
{ id: "payment", name: "Payment failure outreach", status: "Completed", leads: 1120, connectRate: "45%", booked: 151, owner: "Arjun Das", started: "Oct 12", context: "Finished Oct 19", callingWindows: 2, week: week(160, 190, 170, 210, 200, 70, 34) },
|
|
104
|
+
{ id: "festive", name: "Festive offer callbacks", status: "Paused", leads: 640, connectRate: "36%", booked: 52, owner: "Neha Kapoor", started: "Oct 18", context: "Paused yesterday at 18:10", callingWindows: 1, week: week(60, 80, 70, 90, 88, 0, 0) },
|
|
105
|
+
{ id: "abandoned", name: "Abandoned application recovery", status: "Active", leads: 4050, connectRate: "40%", booked: 377, owner: "Sanjay Rao", started: "Oct 18", context: "Running since Oct 18", callingWindows: 3, week: week(340, 380, 400, 420, 410, 330, 310) },
|
|
106
|
+
{ id: "renewal-check", name: "Insurance renewal check-in", status: "Failed", leads: 2210, connectRate: "—", booked: 0, owner: "Kiran Menon", started: "Oct 16", context: "CRM writeback failed on Oct 16", callingWindows: 2, week: week(0, 0, 0, 0, 0, 0, 0) },
|
|
107
|
+
{ id: "credit-limit", name: "Credit limit upgrade offer", status: "Completed", leads: 3480, connectRate: "43%", booked: 298, owner: "Divya Shetty", started: "Oct 08", context: "Finished Oct 15", callingWindows: 3, week: week(300, 320, 310, 360, 340, 120, 80) },
|
|
108
|
+
{ id: "survey", name: "Home loan top-up survey", status: "Active", leads: 2730, connectRate: "35%", booked: 120, owner: "Asha Rao", started: "Oct 24", context: "Running since Oct 24", callingWindows: 1, week: week(0, 0, 120, 180, 210, 190, 160) },
|
|
109
|
+
{ id: "welcome", name: "New account welcome calls", status: "Scheduled", leads: 960, connectRate: "—", booked: 0, owner: "Meera Pillai", started: "Nov 02", context: "Starts Nov 2 at 09:30", callingWindows: 1, week: week(0, 0, 0, 0, 0, 0, 0) },
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
/** Status tabs use the same words as the status chips. */
|
|
113
|
+
const TABS: Array<{ value: "all" | CampaignStatus; label: string }> = [
|
|
114
|
+
{ value: "all", label: "All" },
|
|
115
|
+
{ value: "Draft", label: "Draft" },
|
|
116
|
+
{ value: "Active", label: "Active" },
|
|
117
|
+
{ value: "Paused", label: "Paused" },
|
|
118
|
+
{ value: "Completed", label: "Completed" },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const INITIAL_FILTERS: FilterGroup[] = [
|
|
122
|
+
{
|
|
123
|
+
id: "owner",
|
|
124
|
+
label: "Owner",
|
|
125
|
+
value: "all",
|
|
126
|
+
options: [{ value: "all", label: "All owners" }, ...Array.from(new Set(CAMPAIGNS.map((c) => c.owner))).map((owner) => ({ value: owner, label: owner }))],
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: "windows",
|
|
130
|
+
label: "Calling windows",
|
|
131
|
+
value: "all",
|
|
132
|
+
options: [
|
|
133
|
+
{ value: "all", label: "Any number" },
|
|
134
|
+
{ value: "1", label: "1 window" },
|
|
135
|
+
{ value: "2", label: "2 windows" },
|
|
136
|
+
{ value: "3", label: "3 windows" },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
const ROW_HEIGHT = 64;
|
|
142
|
+
/** Header row + footer (pagination) heights inside the table surface. */
|
|
143
|
+
const TABLE_CHROME = 37 + 53;
|
|
144
|
+
|
|
145
|
+
/** Derive the page size from the height left in the panel, so rows fill it. */
|
|
146
|
+
function useRowsThatFit(ref: React.RefObject<HTMLDivElement | null>) {
|
|
147
|
+
const [rows, setRows] = React.useState(8);
|
|
148
|
+
React.useLayoutEffect(() => {
|
|
149
|
+
const node = ref.current;
|
|
150
|
+
if (!node) return;
|
|
151
|
+
const update = () => setRows(Math.max(3, Math.floor((node.clientHeight - TABLE_CHROME) / ROW_HEIGHT)));
|
|
152
|
+
update();
|
|
153
|
+
const observer = new ResizeObserver(update);
|
|
154
|
+
observer.observe(node);
|
|
155
|
+
return () => observer.disconnect();
|
|
156
|
+
}, [ref]);
|
|
157
|
+
return rows;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function findCampaign(id: string | null): CampaignRecord | undefined {
|
|
161
|
+
if (!id) return undefined;
|
|
162
|
+
for (const campaign of CAMPAIGNS) {
|
|
163
|
+
if (campaign.id === id) return campaign;
|
|
164
|
+
const cohort = campaign.cohorts?.find((child) => child.id === id);
|
|
165
|
+
if (cohort) return cohort;
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function CampaignCells({ campaign }: { campaign: CampaignRecord }) {
|
|
171
|
+
return (
|
|
172
|
+
<>
|
|
173
|
+
<TableCell>{campaign.name}</TableCell>
|
|
174
|
+
<TableCell>
|
|
175
|
+
<Chip color={STATUS_COLOR[campaign.status]}>{campaign.status}</Chip>
|
|
176
|
+
</TableCell>
|
|
177
|
+
<TableCell align="right" className="tabular-nums">{campaign.leads.toLocaleString("en-IN")}</TableCell>
|
|
178
|
+
<TableCell>
|
|
179
|
+
<span className="flex items-center gap-2">
|
|
180
|
+
<Avatar name={campaign.owner} size="sm" />
|
|
181
|
+
{campaign.owner}
|
|
182
|
+
</span>
|
|
183
|
+
</TableCell>
|
|
184
|
+
</>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Selection-driven detail panel (principle `record-detail-panels`). */
|
|
189
|
+
export function CampaignDetail({ campaign, onClose }: { campaign: CampaignRecord | undefined; onClose: () => void }) {
|
|
190
|
+
return (
|
|
191
|
+
<Drawer open={Boolean(campaign)} onOpenChange={(open) => !open && onClose()} aria-label="Campaign details">
|
|
192
|
+
{campaign && (
|
|
193
|
+
<>
|
|
194
|
+
<DrawerHeader>
|
|
195
|
+
<p className="text-xs uppercase tracking-wide text-secondary">Campaign</p>
|
|
196
|
+
<DrawerTitle>{campaign.name}</DrawerTitle>
|
|
197
|
+
{/* Status lives with the title, so it never floats on its own. */}
|
|
198
|
+
<div className="mt-1 flex items-center gap-2">
|
|
199
|
+
<Chip color={STATUS_COLOR[campaign.status]}>{campaign.status}</Chip>
|
|
200
|
+
<span className="text-s text-secondary">{campaign.context}</span>
|
|
201
|
+
</div>
|
|
202
|
+
</DrawerHeader>
|
|
203
|
+
<DrawerBody className="flex flex-col gap-8 overflow-y-auto">
|
|
204
|
+
<section aria-label="Performance" className="flex flex-col gap-2">
|
|
205
|
+
<h3 className="mb-1 text-m font-medium text-foreground">Performance</h3>
|
|
206
|
+
<dl className="grid grid-cols-3 gap-2">
|
|
207
|
+
{[
|
|
208
|
+
{ label: "Leads", value: campaign.leads.toLocaleString("en-IN") },
|
|
209
|
+
{ label: "Connect rate", value: campaign.connectRate },
|
|
210
|
+
{ label: "Booked", value: campaign.booked.toLocaleString("en-IN") },
|
|
211
|
+
].map((stat) => (
|
|
212
|
+
<div key={stat.label} className="min-w-0 rounded-l border border-tertiary p-3">
|
|
213
|
+
<dt className="truncate text-s text-secondary">{stat.label}</dt>
|
|
214
|
+
<dd className="mt-0.5 text-xl tabular-nums text-foreground">{stat.value}</dd>
|
|
215
|
+
</div>
|
|
216
|
+
))}
|
|
217
|
+
</dl>
|
|
218
|
+
<div className="rounded-l border border-tertiary p-4">
|
|
219
|
+
<LineChart
|
|
220
|
+
title={<span className="text-s font-normal text-secondary">Connected calls · last 7 days</span>}
|
|
221
|
+
data={campaign.week.map((calls, index) => ({ day: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][index], calls }))}
|
|
222
|
+
categoryKey="day"
|
|
223
|
+
series={["calls"]}
|
|
224
|
+
seriesLabels={{ calls: "Connected" }}
|
|
225
|
+
legend={false}
|
|
226
|
+
showYAxis={false}
|
|
227
|
+
height={160}
|
|
228
|
+
/>
|
|
229
|
+
</div>
|
|
230
|
+
</section>
|
|
231
|
+
<section aria-label="Details" className="flex flex-col gap-1">
|
|
232
|
+
<h3 className="text-m font-medium text-foreground">Details</h3>
|
|
233
|
+
<dl>
|
|
234
|
+
{[
|
|
235
|
+
{ label: "Owner", value: campaign.owner },
|
|
236
|
+
{ label: "Started", value: campaign.started },
|
|
237
|
+
{ label: "Calling windows", value: String(campaign.callingWindows) },
|
|
238
|
+
].map((row) => (
|
|
239
|
+
<div key={row.label} className="flex min-h-10 items-center justify-between gap-3 border-b border-tertiary last:border-0">
|
|
240
|
+
<dt className="text-m text-secondary">{row.label}</dt>
|
|
241
|
+
<dd className="text-m text-foreground">{row.value}</dd>
|
|
242
|
+
</div>
|
|
243
|
+
))}
|
|
244
|
+
</dl>
|
|
245
|
+
</section>
|
|
246
|
+
</DrawerBody>
|
|
247
|
+
<DrawerFooter>
|
|
248
|
+
<Button variant="outline">Duplicate</Button>
|
|
249
|
+
<Button>Open full view</Button>
|
|
250
|
+
</DrawerFooter>
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
</Drawer>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const SAVED_AUDIENCES = [
|
|
258
|
+
{ id: "0019b431", name: "CSV audience 0019b431", size: 6 },
|
|
259
|
+
{ id: "b190b191", name: "CSV audience b190b191", size: 868 },
|
|
260
|
+
{ id: "5ee46ae6", name: "CSV audience 5ee46ae6", size: 162 },
|
|
261
|
+
{ id: "9dc3d0b1", name: "CSV audience 9dc3d0b1", size: 188 },
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
/** Create task launched from the collection: a Modal over the list (principle `minimal-information`). */
|
|
265
|
+
export function NewCampaignModal({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) {
|
|
266
|
+
const [name, setName] = React.useState("");
|
|
267
|
+
const [useNumber, setUseNumber] = React.useState(false);
|
|
268
|
+
const [audience, setAudience] = React.useState("csv");
|
|
269
|
+
const [query, setQuery] = React.useState("");
|
|
270
|
+
const audiences = SAVED_AUDIENCES.filter((item) => item.name.toLowerCase().includes(query.trim().toLowerCase()));
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<Modal open={open} onOpenChange={onOpenChange} height="fixed">
|
|
274
|
+
<ModalHeader>
|
|
275
|
+
<ModalTitle>New campaign</ModalTitle>
|
|
276
|
+
</ModalHeader>
|
|
277
|
+
<ModalBody className="flex flex-col gap-6">
|
|
278
|
+
<InputField label="Campaign name" value={name} onChange={(event) => setName(event.target.value)} placeholder="First-EMI onboarding" />
|
|
279
|
+
<div className="flex flex-col gap-2">
|
|
280
|
+
<p className="text-m font-medium text-foreground">Caller numbers</p>
|
|
281
|
+
<div className="flex min-h-12 items-center gap-3 rounded-m border border-input px-3">
|
|
282
|
+
<Checkbox aria-label="+91 22 6987 0900" checked={useNumber} onChange={(event) => setUseNumber(event.target.checked)} />
|
|
283
|
+
<span className="flex-1 text-m tabular-nums text-foreground">+91 22 6987 0900</span>
|
|
284
|
+
<Chip>Plivo</Chip>
|
|
285
|
+
</div>
|
|
286
|
+
<p className="text-s text-secondary">Optional. Tick none to call from any enabled number; you can change this later, even while it is calling.</p>
|
|
287
|
+
</div>
|
|
288
|
+
<div className="flex flex-col gap-3">
|
|
289
|
+
<p className="text-m font-medium text-foreground">Audience</p>
|
|
290
|
+
<SegmentedControl
|
|
291
|
+
aria-label="Audience source"
|
|
292
|
+
options={[{ value: "csv", label: "CSV upload" }, { value: "empty", label: "Empty audience" }]}
|
|
293
|
+
value={audience}
|
|
294
|
+
onChange={setAudience}
|
|
295
|
+
/>
|
|
296
|
+
{audience === "csv" && (
|
|
297
|
+
<>
|
|
298
|
+
<InputField aria-label="Search audiences" type="search" placeholder="Search audiences" value={query} onChange={(event) => setQuery(event.target.value)} />
|
|
299
|
+
<FileDropzone accept=".csv,.xlsx" hint="CSV or first-sheet .xlsx with a phone column; name and other columns are optional" />
|
|
300
|
+
<ul aria-label="Saved audiences" className="rounded-m border border-input px-3">
|
|
301
|
+
{audiences.map((item) => (
|
|
302
|
+
<li key={item.id} className="flex min-h-10 items-center justify-between gap-3 border-b border-tertiary text-m last:border-0">
|
|
303
|
+
<span className="text-foreground">{item.name}</span>
|
|
304
|
+
<span className="tabular-nums text-secondary">{item.size.toLocaleString("en-IN")}</span>
|
|
305
|
+
</li>
|
|
306
|
+
))}
|
|
307
|
+
</ul>
|
|
308
|
+
</>
|
|
309
|
+
)}
|
|
310
|
+
</div>
|
|
311
|
+
</ModalBody>
|
|
312
|
+
<ModalFooter>
|
|
313
|
+
<Button variant="outline" onClick={() => onOpenChange(false)}>Cancel</Button>
|
|
314
|
+
<Button disabled={!name.trim()} onClick={() => onOpenChange(false)}>Create campaign</Button>
|
|
315
|
+
</ModalFooter>
|
|
316
|
+
</Modal>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function CampaignsScreen() {
|
|
321
|
+
const [tab, setTab] = React.useState<"all" | CampaignStatus>("all");
|
|
322
|
+
const [search, setSearch] = React.useState("");
|
|
323
|
+
const [searchOpen, setSearchOpen] = React.useState(false);
|
|
324
|
+
const [filters, setFilters] = React.useState(INITIAL_FILTERS);
|
|
325
|
+
const [selectedKeys, setSelectedKeys] = React.useState<Set<string>>(new Set());
|
|
326
|
+
const [openId, setOpenId] = React.useState<string | null>(null);
|
|
327
|
+
const [page, setPage] = React.useState(1);
|
|
328
|
+
const [creating, setCreating] = React.useState(false);
|
|
329
|
+
const rowsRef = React.useRef<HTMLDivElement>(null);
|
|
330
|
+
const pageSize = useRowsThatFit(rowsRef);
|
|
331
|
+
|
|
332
|
+
const owner = filters.find((group) => group.id === "owner")?.value ?? "all";
|
|
333
|
+
const windows = filters.find((group) => group.id === "windows")?.value ?? "all";
|
|
334
|
+
const query = search.trim().toLowerCase();
|
|
335
|
+
const visible = CAMPAIGNS.filter(
|
|
336
|
+
(c) =>
|
|
337
|
+
(tab === "all" || c.status === tab) &&
|
|
338
|
+
(owner === "all" || c.owner === owner) &&
|
|
339
|
+
(windows === "all" || String(c.callingWindows) === windows) &&
|
|
340
|
+
(!query || c.name.toLowerCase().includes(query)),
|
|
341
|
+
);
|
|
342
|
+
const pages = Math.max(1, Math.ceil(visible.length / pageSize));
|
|
343
|
+
const current = Math.min(page, pages);
|
|
344
|
+
const pageRows = visible.slice((current - 1) * pageSize, current * pageSize);
|
|
345
|
+
const countFor = (value: "all" | CampaignStatus) =>
|
|
346
|
+
value === "all" ? CAMPAIGNS.length : CAMPAIGNS.filter((c) => c.status === value).length;
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div className="flex h-screen gap-3 bg-background p-3 text-foreground">
|
|
350
|
+
<section aria-label="Voice campaigns" className="flex min-w-0 flex-1 flex-col overflow-hidden rounded-l bg-card">
|
|
351
|
+
<BodyPageHeader title="Voice campaigns" actions={<Button onClick={() => setCreating(true)}>New campaign</Button>} />
|
|
352
|
+
|
|
353
|
+
{/* Tabs lead; search and filters trail as icon triggers. */}
|
|
354
|
+
<div className="flex items-center justify-between gap-4 px-6">
|
|
355
|
+
<Tabs className="min-w-0 flex-1" value={tab} onValueChange={(value) => { setTab(value as typeof tab); setPage(1); }}>
|
|
356
|
+
<TabList aria-label="Filter campaigns by status">
|
|
357
|
+
{TABS.map((item) => (
|
|
358
|
+
<Tab key={item.value} value={item.value} count={countFor(item.value)}>
|
|
359
|
+
{item.label}
|
|
360
|
+
</Tab>
|
|
361
|
+
))}
|
|
362
|
+
</TabList>
|
|
363
|
+
</Tabs>
|
|
364
|
+
<div className="flex shrink-0 items-center gap-2">
|
|
365
|
+
<SearchDisclosure
|
|
366
|
+
value={search}
|
|
367
|
+
onValueChange={(value) => { setSearch(value); setPage(1); }}
|
|
368
|
+
open={searchOpen}
|
|
369
|
+
onOpenChange={setSearchOpen}
|
|
370
|
+
placeholder="Search campaigns"
|
|
371
|
+
triggerLabel="Search campaigns"
|
|
372
|
+
/>
|
|
373
|
+
<Filters
|
|
374
|
+
triggerLabel="Filter campaigns"
|
|
375
|
+
groups={filters}
|
|
376
|
+
onChange={(id, value) => { setFilters((groups) => groups.map((group) => (group.id === id ? { ...group, value } : group))); setPage(1); }}
|
|
377
|
+
/>
|
|
378
|
+
</div>
|
|
379
|
+
</div>
|
|
380
|
+
|
|
381
|
+
{/* The rows fill what is left of the panel; pagination sits at the bottom. */}
|
|
382
|
+
<div ref={rowsRef} className="min-h-0 flex-1 px-6 pt-4 pb-6">
|
|
383
|
+
<Table
|
|
384
|
+
aria-label="Voice campaigns"
|
|
385
|
+
selectionMode="multiple"
|
|
386
|
+
selectedKeys={selectedKeys}
|
|
387
|
+
onSelectionChange={setSelectedKeys}
|
|
388
|
+
footer={
|
|
389
|
+
<Pagination
|
|
390
|
+
className="w-full"
|
|
391
|
+
total={pages}
|
|
392
|
+
page={current}
|
|
393
|
+
onChange={setPage}
|
|
394
|
+
summary={`${visible.length === 0 ? 0 : (current - 1) * pageSize + 1}–${Math.min(current * pageSize, visible.length)} of ${visible.length} campaigns`}
|
|
395
|
+
/>
|
|
396
|
+
}
|
|
397
|
+
>
|
|
398
|
+
<TableHeader>
|
|
399
|
+
<TableRow>
|
|
400
|
+
<TableColumn>Campaign</TableColumn>
|
|
401
|
+
<TableColumn>Status</TableColumn>
|
|
402
|
+
<TableColumn align="right">Leads</TableColumn>
|
|
403
|
+
<TableColumn>Owner</TableColumn>
|
|
404
|
+
</TableRow>
|
|
405
|
+
</TableHeader>
|
|
406
|
+
<TableBody>
|
|
407
|
+
{pageRows.map((campaign) => (
|
|
408
|
+
<TableRow
|
|
409
|
+
key={campaign.id}
|
|
410
|
+
rowKey={campaign.id}
|
|
411
|
+
aria-label={`Open ${campaign.name}`}
|
|
412
|
+
isActive={openId === campaign.id}
|
|
413
|
+
onClick={() => setOpenId((id) => (id === campaign.id ? null : campaign.id))}
|
|
414
|
+
childRows={campaign.cohorts?.map((cohort) => (
|
|
415
|
+
<TableRow
|
|
416
|
+
key={cohort.id}
|
|
417
|
+
rowKey={cohort.id}
|
|
418
|
+
aria-label={`Open ${cohort.name}`}
|
|
419
|
+
isActive={openId === cohort.id}
|
|
420
|
+
onClick={() => setOpenId((id) => (id === cohort.id ? null : cohort.id))}
|
|
421
|
+
>
|
|
422
|
+
<CampaignCells campaign={cohort} />
|
|
423
|
+
</TableRow>
|
|
424
|
+
))}
|
|
425
|
+
defaultExpanded={Boolean(campaign.cohorts)}
|
|
426
|
+
>
|
|
427
|
+
<CampaignCells campaign={campaign} />
|
|
428
|
+
</TableRow>
|
|
429
|
+
))}
|
|
430
|
+
</TableBody>
|
|
431
|
+
</Table>
|
|
432
|
+
</div>
|
|
433
|
+
</section>
|
|
434
|
+
|
|
435
|
+
<CampaignDetail campaign={findCampaign(openId)} onClose={() => setOpenId(null)} />
|
|
436
|
+
<NewCampaignModal open={creating} onOpenChange={setCreating} />
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
}
|
package/lib/icons.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs as p, Fragment as d, jsx as a } from "react/jsx-runtime";
|
|
3
3
|
import * as r from "react";
|
|
4
|
-
import { Code as $, Check as k, Copy as S, FileText as f, UploadSimple as P, X as T, EyeSlash as w, Eye as L, MagnifyingGlass as A, Minus as m, Plus as x, CaretDown as B,
|
|
4
|
+
import { Code as $, Check as k, Copy as S, FileText as f, UploadSimple as P, X as T, EyeSlash as w, Eye as L, MagnifyingGlass as A, Minus as m, Plus as x, CaretDown as B, FunnelSimple as I, Info as y, WarningCircle as U, Warning as F, CheckCircle as M, CaretRight as D, CaretUp as O, ArrowsDownUp as b, ChatCircle as R, ArrowUpRight as v, Clock as H, Database as E, ArrowSquareOut as G, Table as W, CaretLeft as z, CornersOut as V, Magnet as N, Tray as j, ArrowLeft as X, DotsThree as Q, User as q, XCircle as _, CircleDashed as J, ArrowsClockwise as K, Paperclip as Y, Quotes as Z, At as ee, Lightning as oe, Sparkle as te, File as ne, DownloadSimple as se, Image as C, BracketsCurly as ae, MusicNotes as re, VideoCamera as ce, ImageBroken as ie, Pulse as le, Archive as he, ArrowDown as Ce, ArrowRight as ge, ArrowUp as ue, Scroll as pe, Bell as de, Stack as $e, BookOpen as ke, Brain as Se, CalendarBlank as fe, Phone as Pe, ListChecks as Te, Cube as we, DotsNine as Le, Folder as Ae, FolderOpen as me, Gauge as xe, GitBranch as Be, Hash as Ie, ClockCounterClockwise as ye, House as Ue, SquaresFour as Fe, Leaf as Me, Lightbulb as De, Link as Oe, CircleNotch as be, SignOut as Re, Envelope as ve, Microphone as He, MicrophoneSlash as Ee, DotsThreeVertical as Ge, Palette as We, SidebarSimple as ze, Pause as Ve, PencilSimple as Ne, PhoneIncoming as je, PhoneSlash as Xe, ChartPie as Qe, Play as qe, Plug as _e, Presentation as Je, Rocket as Ke, Gear as Ye, Shapes as Ze, ShareNetwork as eo, Star as oo, Stop as to, ArrowsLeftRight as no, Target as so, Binoculars as ao, TextB as ro, TextT as co, TextItalic as io, TextStrikethrough as lo, TextUnderline as ho, ToggleRight as Co, Trash as go, Users as uo, Wrench as po } from "@phosphor-icons/react";
|
|
5
5
|
const $o = r.createContext(!1), ko = /* @__PURE__ */ new Set(["Info"]), So = new Set(
|
|
6
6
|
[
|
|
7
7
|
"FunnelSimple",
|
|
@@ -58,10 +58,10 @@ function e(o, c) {
|
|
|
58
58
|
});
|
|
59
59
|
return i.displayName = c, i;
|
|
60
60
|
}
|
|
61
|
-
const wo = e(
|
|
61
|
+
const wo = e(Ce, "ArrowDown"), Lo = e(X, "ArrowLeft"), Ao = e(ge, "ArrowRight"), mo = e(ue, "ArrowUp"), xo = e(v, "ArrowUpRight"), Bo = e(B, "ChevronDown"), Io = e(z, "ChevronLeft"), yo = e(D, "ChevronRight"), Uo = e(O, "ChevronUp"), Fo = e(b, "SortIcon"), Mo = e(no, "SwapHorizontal"), Do = e(G, "ExternalLink"), Oo = e(ze, "PanelLeft"), bo = e(ee, "At"), Ro = e(Pe, "Call"), vo = e(je, "PhoneIncoming"), Ho = e(T, "Close"), Eo = e(_, "CloseCircle"), Go = e(S, "Copy"), Wo = e(se, "Download"), zo = e(Re, "LogOut"), Vo = e(He, "Mic"), No = e(Ee, "MicOff"), jo = e(Xe, "PhoneOff"), Xo = e(Ne, "Pencil"), Qo = e(Z, "Quote"), qo = e(L, "Eye"), _o = e(w, "EyeOff"), Jo = e(Ie, "Hash"), Ko = e(C, "ImageUpload"), Yo = e(P, "Upload"), Zo = e(co, "TextFont"), et = e(ro, "TextBold"), ot = e(io, "TextItalicStyle"), tt = e(lo, "TextStrike"), nt = e(
|
|
62
62
|
ho,
|
|
63
63
|
"TextUnderlineStyle"
|
|
64
|
-
), st = e(Co, "ToggleOn"), at = e(
|
|
64
|
+
), st = e(Co, "ToggleOn"), at = e(fe, "Calendar"), rt = e(H, "Clock"), ct = e($, "Code"), it = e(to, "Stop"), lt = e(N, "Magnet"), ht = e(V, "Maximize"), Ct = e(m, "Minus"), gt = e(Q, "MoreHorizontal"), ut = e(Ge, "MoreVertical"), pt = e(Le, "DragHandle"), dt = e(Oe, "Link"), $t = e(x, "Plus"), kt = e(K, "Refresh"), St = e(pe, "AuditTrail"), ft = e(A, "Search"), Pt = e(eo, "Share"), Tt = e(go, "Trash"), wt = e(k, "Check"), Lt = e(be, "Loader"), At = e(M, "CheckCircle"), mt = e(y, "Info"), xt = e(F, "Warning"), Bt = e(de, "Bell"), It = e(U, "WarningCircle"), yt = e(le, "Activity"), Ut = e(R, "AgentChat"), Ft = e(oe, "Bolt"), Mt = e(ke, "BookOpen"), Dt = e(Se, "Brain"), Ot = e(Te, "CheckList"), bt = e(we, "Component"), Rt = e(E, "Database"), vt = e(W, "Table"), Ht = e(I, "Filter"), Et = e(he, "Archive"), Gt = e(ne, "File"), Wt = e(f, "FileText"), zt = e(Ae, "Folder"), Vt = e(C, "Image"), Nt = e(ie, "ImageOff"), jt = e(re, "Music"), Xt = e(ce, "Video"), Qt = e(ae, "Braces"), qt = e(me, "FolderOpen"), _t = e(xe, "Gauge"), Jt = e(Be, "GitBranch"), Kt = e(ye, "HistoryClock"), Yt = e(Ue, "Home"), Zt = e($e, "Blocks"), en = e(j, "Inbox"), on = e(De, "Lightbulb"), tn = e(Me, "Leaf"), nn = e(Fe, "LayoutGrid"), sn = e(ve, "Mail"), an = e(We, "Palette"), rn = e(Qe, "PieChart"), cn = e(_e, "Plug"), ln = e(Ke, "Rocket"), hn = e(Ye, "Settings"), Cn = e(Ze, "Shapes"), gn = e(te, "Sparkles"), un = e(oo, "Star"), pn = e(ao, "Telescope"), dn = e(so, "Target"), $n = e(q, "User"), kn = e(uo, "Users"), Sn = e(qe, "Play"), fn = e(Je, "Presentation"), Pn = e(Ve, "Pause"), Tn = e(Y, "Paperclip"), wn = e(J, "CircleDashed"), Ln = e(po, "Wrench");
|
|
65
65
|
export {
|
|
66
66
|
yt as Activity,
|
|
67
67
|
Ut as AgentChat,
|