@murumets-ee/create 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -1,15 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
  import*as e from"@clack/prompts";import t from"picocolors";import{dirname as n,join as r}from"node:path";import{execSync as i}from"node:child_process";import{appendFile as a,mkdir as o,readFile as s,rm as c,writeFile as l}from"node:fs/promises";var u=Object.defineProperty,d=(e,t)=>{let n={};for(var r in e)u(n,r,{get:e[r],enumerable:!0});return t||u(n,Symbol.toStringTag,{value:`Module`}),n};function f(e){let t={},n=e.slice(2);for(let e=0;e<n.length;e++){let r=n[e];if(r===`--mode`&&n[e+1]){let r=n[++e];(r===`single`||r===`multi`)&&(t.mode=r)}else r===`--install`?t.install=!0:r===`--no-install`?t.install=!1:!r.startsWith(`-`)&&!t.name&&(t.name=r)}return t}async function p(t){let n=f(process.argv),r=n.name??t;if(r&&n.mode&&n.install!==void 0)return{name:r,mode:n.mode,installDeps:n.install};e.intro(`@murumets-ee/create`);let i=r??await e.text({message:`Project name:`,placeholder:`my-project`,validate:e=>{if(!e)return`Project name is required`;if(!/^[a-z0-9-]+$/.test(e))return`Use lowercase letters, numbers, and hyphens only`}});if(e.isCancel(i))return e.cancel(`Cancelled.`),null;let a=n.mode??await e.select({message:`App setup:`,options:[{value:`single`,label:`Single app`,hint:`everything in one Next.js project`},{value:`multi`,label:`Admin + Frontend`,hint:`two apps in a pnpm workspace`}]});if(e.isCancel(a))return e.cancel(`Cancelled.`),null;let o=n.install??await e.confirm({message:`Install dependencies?`,initialValue:!0});return e.isCancel(o)?(e.cancel(`Cancelled.`),null):{name:i,mode:a,installDeps:o}}var m=d({appendToFile:()=>y,deleteFiles:()=>_,mergePackageJson:()=>g,runCommand:()=>v,writeFile:()=>h});async function h(e,t){await o(n(e),{recursive:!0}),await l(e,t,`utf-8`)}async function g(e,t){let n=await s(e,`utf-8`),r=JSON.parse(n);t.type&&(r.type=t.type),t.dependencies&&(r.dependencies={...r.dependencies,...t.dependencies}),t.devDependencies&&(r.devDependencies={...r.devDependencies,...t.devDependencies}),t.scripts&&(r.scripts={...r.scripts,...t.scripts}),await l(e,`${JSON.stringify(r,null,2)}\n`,`utf-8`)}async function _(e,t){for(let n of t)await c(r(e,n),{force:!0,recursive:!0})}function v(e,t){return i(e,{cwd:t?.cwd,stdio:`pipe`,encoding:`utf-8`})}async function y(e,t){await a(e,t,`utf-8`)}async function b(e,t){await h(r(e,`lib/admin-config.ts`),`import { Media } from '@murumets-ee/media'
3
+ import {
4
+ Ticket,
5
+ TicketMessage,
6
+ TicketAttachment,
7
+ Department,
8
+ TicketTag,
9
+ } from '@murumets-ee/ticketing'
3
10
  import { Article, Category } from '@/entities'
4
11
 
5
12
  /** All entities available in the admin (drives sidebar nav + CRUD) */
6
- export const allEntities = [Article, Media, Category]
13
+ export const allEntities = [
14
+ Article, Media, Category,
15
+ Ticket, TicketMessage, TicketAttachment, Department, TicketTag,
16
+ ]
7
17
 
8
18
  /** Taxonomy entities keyed by name */
9
- export const taxonomyVocabularies = { category: Category }
19
+ export const taxonomyVocabularies = {
20
+ category: Category,
21
+ department: Department,
22
+ ticket_tag: TicketTag,
23
+ }
10
24
 
11
25
  /** Entities exposed via generic CRUD API handler */
12
- export const crudEntities = [Article, Media]
26
+ export const crudEntities = [Article, Media, Ticket, TicketMessage, TicketAttachment]
13
27
 
14
28
  /** Plugin resources for the permission catalog */
15
29
  export const pluginResources = [
@@ -17,6 +31,7 @@ export const pluginResources = [
17
31
  { resource: 'settings', actions: ['view', 'update'] },
18
32
  { resource: 'audit-logs', actions: ['view'] },
19
33
  { resource: 'permissions', actions: ['view', 'create', 'update', 'delete'] },
34
+ { resource: 'ticketing', actions: ['view', 'create', 'update', 'delete'] },
20
35
  ]
21
36
  `),await h(r(e,`lib/content-locale.ts`),`import { cookies } from 'next/headers'
22
37
  import { hasLocale } from 'next-intl'
@@ -107,15 +122,20 @@ import { AdminShell } from '@murumets-ee/admin-ui'
107
122
  import type { AdminNavGroup } from '@murumets-ee/admin-ui/server'
108
123
  import {
109
124
  Activity,
125
+ Building2,
110
126
  FileText,
111
127
  FolderTree,
112
128
  Home,
113
129
  Image,
114
130
  ImageDown,
131
+ Inbox,
132
+ Kanban,
115
133
  Lock,
116
134
  PenTool,
117
135
  ShieldCheck,
136
+ Tag,
118
137
  Tags,
138
+ Ticket,
119
139
  Users,
120
140
  type LucideIcon,
121
141
  } from 'lucide-react'
@@ -127,6 +147,7 @@ const ICON_MAP: Record<string, LucideIcon> = {
127
147
  'file-text': FileText,
128
148
  'folder-tree': FolderTree,
129
149
  tags: Tags,
150
+ ticket: Ticket,
130
151
  }
131
152
 
132
153
  interface AdminLayoutProps {
@@ -162,6 +183,16 @@ export function AdminLayout({
162
183
  ],
163
184
  }
164
185
 
186
+ const supportGroup: SidebarNavGroup = {
187
+ label: 'Support',
188
+ items: [
189
+ { label: 'Tickets', href: '/admin/tickets', icon: Inbox },
190
+ { label: 'Board', href: '/admin/tickets/board', icon: Kanban },
191
+ { label: 'Departments', href: '/admin/departments', icon: Building2 },
192
+ { label: 'Ticket Tags', href: '/admin/ticket-tags', icon: Tag },
193
+ ],
194
+ }
195
+
165
196
  const systemGroup: SidebarNavGroup = {
166
197
  label: 'System',
167
198
  items: [
@@ -177,6 +208,7 @@ export function AdminLayout({
177
208
  const navGroups: SidebarNavGroup[] = [
178
209
  staticBefore,
179
210
  ...dynamicGroups,
211
+ supportGroup,
180
212
  systemGroup,
181
213
  ]
182
214
 
@@ -1017,6 +1049,432 @@ export default async function ActivityPage({ params }: { params: Promise<{ local
1017
1049
  )
1018
1050
  })
1019
1051
  }
1052
+ `),await h(r(e,`app/[locale]/(shell)/admin/tickets/page.tsx`),`import { Ticket, Department, TicketTag } from '@murumets-ee/ticketing'
1053
+ import { headers } from 'next/headers'
1054
+ import { setRequestLocale } from 'next-intl/server'
1055
+ import { withAdminContext } from '@/lib/with-admin-context'
1056
+ import { auth } from '@/lib/auth'
1057
+ import { TicketsInboxClient } from './tickets-inbox-client'
1058
+ import { fetchEntityList } from '@murumets-ee/admin-ui/server'
1059
+ import type {
1060
+ TicketData,
1061
+ DepartmentData,
1062
+ TagData,
1063
+ } from '@murumets-ee/ticketing-ui'
1064
+
1065
+ interface TicketsPageProps {
1066
+ params: Promise<{ locale: string }>
1067
+ }
1068
+
1069
+ export default async function TicketsPage({ params }: TicketsPageProps) {
1070
+ const { locale } = await params
1071
+ setRequestLocale(locale)
1072
+
1073
+ const h = await headers()
1074
+ const session = await auth.api.getSession({ headers: h })
1075
+ const currentUserId = session?.user?.id ?? ''
1076
+
1077
+ return withAdminContext(locale, async () => {
1078
+ const ticketData = await fetchEntityList(Ticket, {
1079
+ sortField: 'lastReplyAt',
1080
+ sortDirection: 'desc',
1081
+ limit: 50,
1082
+ })
1083
+
1084
+ const departmentData = await fetchEntityList(Department, {
1085
+ sortField: 'name',
1086
+ sortDirection: 'asc',
1087
+ limit: 100,
1088
+ })
1089
+
1090
+ const tagData = await fetchEntityList(TicketTag, {
1091
+ sortField: 'name',
1092
+ sortDirection: 'asc',
1093
+ limit: 100,
1094
+ })
1095
+
1096
+ return (
1097
+ <TicketsInboxClient
1098
+ initialTickets={ticketData.items as unknown as TicketData[]}
1099
+ initialTotal={ticketData.total}
1100
+ initialDepartments={departmentData.items as unknown as DepartmentData[]}
1101
+ initialTags={tagData.items as unknown as TagData[]}
1102
+ currentUserId={currentUserId}
1103
+ locale={locale}
1104
+ />
1105
+ )
1106
+ })
1107
+ }
1108
+ `),await h(r(e,`app/[locale]/(shell)/admin/tickets/tickets-inbox-client.tsx`),`'use client'
1109
+
1110
+ import { QueryProvider } from '@murumets-ee/admin-ui'
1111
+ import { InboxProvider, TicketInbox } from '@murumets-ee/ticketing-ui/inbox'
1112
+ import type {
1113
+ TicketData,
1114
+ DepartmentData,
1115
+ TagData,
1116
+ } from '@murumets-ee/ticketing-ui'
1117
+
1118
+ interface TicketsInboxClientProps {
1119
+ initialTickets: TicketData[]
1120
+ initialTotal: number
1121
+ initialDepartments: DepartmentData[]
1122
+ initialTags: TagData[]
1123
+ currentUserId: string
1124
+ locale: string
1125
+ }
1126
+
1127
+ export function TicketsInboxClient({
1128
+ initialTickets,
1129
+ initialTotal,
1130
+ initialDepartments,
1131
+ initialTags,
1132
+ currentUserId,
1133
+ }: TicketsInboxClientProps) {
1134
+ return (
1135
+ <QueryProvider>
1136
+ <InboxProvider
1137
+ apiBasePath="/api/admin"
1138
+ initialTickets={initialTickets}
1139
+ initialTotal={initialTotal}
1140
+ initialDepartments={initialDepartments}
1141
+ initialTags={initialTags}
1142
+ currentUserId={currentUserId}
1143
+ >
1144
+ <div className="h-[calc(100vh-3.5rem)]">
1145
+ <TicketInbox currentUserId={currentUserId} />
1146
+ </div>
1147
+ </InboxProvider>
1148
+ </QueryProvider>
1149
+ )
1150
+ }
1151
+ `),await h(r(e,`app/[locale]/(shell)/admin/tickets/board/page.tsx`),`import { Ticket, Department } from '@murumets-ee/ticketing'
1152
+ import { setRequestLocale } from 'next-intl/server'
1153
+ import { withAdminContext } from '@/lib/with-admin-context'
1154
+ import { fetchEntityList } from '@murumets-ee/admin-ui/server'
1155
+ import { TicketBoardClient } from './ticket-board-client'
1156
+ import type { TicketData, DepartmentData } from '@murumets-ee/ticketing-ui'
1157
+
1158
+ interface TicketBoardPageProps {
1159
+ params: Promise<{ locale: string }>
1160
+ }
1161
+
1162
+ export default async function TicketBoardPage({ params }: TicketBoardPageProps) {
1163
+ const { locale } = await params
1164
+ setRequestLocale(locale)
1165
+
1166
+ return withAdminContext(locale, async () => {
1167
+ const ticketData = await fetchEntityList(Ticket, {
1168
+ sortField: 'lastReplyAt',
1169
+ sortDirection: 'desc',
1170
+ limit: 200,
1171
+ })
1172
+
1173
+ const departmentData = await fetchEntityList(Department, {
1174
+ sortField: 'name',
1175
+ sortDirection: 'asc',
1176
+ limit: 100,
1177
+ })
1178
+
1179
+ return (
1180
+ <div className="p-6 h-[calc(100vh-3.5rem)]">
1181
+ <div className="mb-4">
1182
+ <h1 className="text-2xl font-bold">Ticket Board</h1>
1183
+ <p className="text-sm text-muted-foreground">
1184
+ Kanban view of tickets by status
1185
+ </p>
1186
+ </div>
1187
+ <TicketBoardClient
1188
+ initialTickets={ticketData.items as unknown as TicketData[]}
1189
+ initialDepartments={departmentData.items as unknown as DepartmentData[]}
1190
+ locale={locale}
1191
+ />
1192
+ </div>
1193
+ )
1194
+ })
1195
+ }
1196
+ `),await h(r(e,`app/[locale]/(shell)/admin/tickets/board/ticket-board-client.tsx`),`'use client'
1197
+
1198
+ import { useMemo } from 'react'
1199
+ import { TicketBoard } from '@murumets-ee/ticketing-ui/board'
1200
+ import type { TicketData, DepartmentData } from '@murumets-ee/ticketing-ui'
1201
+ import { useRouter } from 'next/navigation'
1202
+
1203
+ interface TicketBoardClientProps {
1204
+ initialTickets: TicketData[]
1205
+ initialDepartments: DepartmentData[]
1206
+ locale: string
1207
+ }
1208
+
1209
+ export function TicketBoardClient({
1210
+ initialTickets,
1211
+ initialDepartments,
1212
+ locale,
1213
+ }: TicketBoardClientProps) {
1214
+ const router = useRouter()
1215
+ const departmentMap = useMemo(
1216
+ () => new Map(initialDepartments.map((d) => [d.id, d])),
1217
+ [initialDepartments],
1218
+ )
1219
+
1220
+ return (
1221
+ <TicketBoard
1222
+ tickets={initialTickets}
1223
+ departments={departmentMap}
1224
+ onTicketSelect={(ticket) => {
1225
+ router.push(\`/\${locale}/admin/tickets?selected=\${ticket.id}\`)
1226
+ }}
1227
+ className="h-[calc(100%-4rem)]"
1228
+ />
1229
+ )
1230
+ }
1231
+ `),await h(r(e,`app/api/ticketing/inbound/route.ts`),`/**
1232
+ * Resend inbound email webhook endpoint.
1233
+ *
1234
+ * Public endpoint (no session auth) — protected by webhook signature
1235
+ * verification (HMAC-SHA256). Lives outside the admin API handler
1236
+ * because the admin handler requires authentication.
1237
+ */
1238
+
1239
+ import { getToolkitApp } from '@/lib/app'
1240
+
1241
+ await getToolkitApp()
1242
+
1243
+ export async function POST(req: Request): Promise<Response> {
1244
+ const { handleInboundWebhook } = await import('@murumets-ee/ticketing')
1245
+ const { QueueClient } = await import('@murumets-ee/queue/client')
1246
+ const { getApp } = await import('@murumets-ee/core')
1247
+
1248
+ const db = getApp().db.readWrite
1249
+ const queueClient = new QueueClient({ db })
1250
+
1251
+ return handleInboundWebhook(req, (type, payload) => queueClient.enqueue(type, payload))
1252
+ }
1253
+ `),await h(r(e,`app/[locale]/(shell)/admin/seed/actions.ts`),`'use server'
1254
+
1255
+ import { AdminClient } from '@murumets-ee/entity/admin'
1256
+ import { createTaxonomyClient } from '@murumets-ee/taxonomy/client'
1257
+ import { Ticket, TicketMessage, Department, TicketTag } from '@murumets-ee/ticketing'
1258
+ import { headers } from 'next/headers'
1259
+ import { Article, Category } from '@/entities'
1260
+ import { getToolkitApp } from '@/lib/app'
1261
+ import { auth } from '@/lib/auth'
1262
+
1263
+ async function requireAdmin() {
1264
+ const session = await auth.api.getSession({ headers: await headers() })
1265
+ if (!session?.user) throw new Error('Unauthorized')
1266
+ const role = (session.user as Record<string, unknown>).role as string | undefined
1267
+ if (role !== 'admin') throw new Error('Forbidden: admin role required')
1268
+ }
1269
+
1270
+ export async function seedContent() {
1271
+ if (process.env.NODE_ENV === 'production') {
1272
+ return { error: 'Seed disabled in production' }
1273
+ }
1274
+
1275
+ await requireAdmin()
1276
+ const app = await getToolkitApp()
1277
+ const categories = createTaxonomyClient(Category)
1278
+ const articles = new AdminClient({
1279
+ entity: Article,
1280
+ db: app.db.readWrite,
1281
+ logger: app.logger.child({ entity: 'article' }),
1282
+ })
1283
+
1284
+ const techCat = await categories.create({ name: 'Technology', slug: 'technology', color: '#3B82F6' })
1285
+ const newsCat = await categories.create({ name: 'News', slug: 'news', color: '#EF4444' })
1286
+
1287
+ await articles.create({
1288
+ title: 'Getting Started with Lumi CMS',
1289
+ slug: 'getting-started',
1290
+ excerpt: 'Learn how to build apps with the toolkit',
1291
+ body: [{ type: 'p', children: [{ text: 'A guide to getting started with Lumi CMS.' }] }],
1292
+ category: techCat.id as string,
1293
+ status: 'published',
1294
+ publishedAt: new Date(),
1295
+ })
1296
+
1297
+ await articles.create({
1298
+ title: 'Lumi CMS v1.0 Released',
1299
+ slug: 'v1-released',
1300
+ excerpt: 'We are excited to announce version 1.0',
1301
+ body: [{ type: 'p', children: [{ text: 'After months of development, v1.0 is here.' }] }],
1302
+ category: newsCat.id as string,
1303
+ status: 'published',
1304
+ publishedAt: new Date(),
1305
+ })
1306
+
1307
+ return { ok: true, created: { categories: 2, articles: 2 } }
1308
+ }
1309
+
1310
+ export async function seedTicketing() {
1311
+ if (process.env.NODE_ENV === 'production') {
1312
+ return { error: 'Seed disabled in production' }
1313
+ }
1314
+
1315
+ await requireAdmin()
1316
+ const app = await getToolkitApp()
1317
+
1318
+ const deptClient = createTaxonomyClient(Department)
1319
+ const tagClient = createTaxonomyClient(TicketTag)
1320
+ const ticketAdmin = new AdminClient({
1321
+ entity: Ticket,
1322
+ db: app.db.readWrite,
1323
+ logger: app.logger.child({ entity: 'ticket' }),
1324
+ })
1325
+ const messageAdmin = new AdminClient({
1326
+ entity: TicketMessage,
1327
+ db: app.db.readWrite,
1328
+ logger: app.logger.child({ entity: 'ticket_message' }),
1329
+ })
1330
+
1331
+ const support = await deptClient.create({
1332
+ name: 'General Support', slug: 'general-support', color: '#3B82F6',
1333
+ description: 'General customer support inquiries',
1334
+ emailAddress: 'support@example.com',
1335
+ slaFirstResponseHours: 4, slaResolutionHours: 24,
1336
+ })
1337
+ const billing = await deptClient.create({
1338
+ name: 'Billing', slug: 'billing', color: '#10B981',
1339
+ description: 'Payment and invoice questions',
1340
+ emailAddress: 'billing@example.com',
1341
+ slaFirstResponseHours: 2, slaResolutionHours: 12,
1342
+ })
1343
+ const technical = await deptClient.create({
1344
+ name: 'Technical', slug: 'technical', color: '#8B5CF6',
1345
+ description: 'Technical issues and bug reports',
1346
+ emailAddress: 'tech@example.com',
1347
+ slaFirstResponseHours: 1, slaResolutionHours: 8,
1348
+ })
1349
+
1350
+ const bugTag = await tagClient.create({ name: 'Bug', slug: 'bug', color: '#EF4444' })
1351
+ const featureTag = await tagClient.create({ name: 'Feature Request', slug: 'feature-request', color: '#F59E0B' })
1352
+ const urgentTag = await tagClient.create({ name: 'Urgent', slug: 'urgent', color: '#DC2626' })
1353
+ await tagClient.create({ name: 'Feedback', slug: 'feedback', color: '#06B6D4' })
1354
+ await tagClient.create({ name: 'Documentation', slug: 'documentation', color: '#6366F1' })
1355
+
1356
+ const now = Date.now()
1357
+ let counter = 0
1358
+
1359
+ async function createTicket(opts: {
1360
+ subject: string; department: string; status: string; priority: string;
1361
+ requesterEmail: string; requesterName: string; tags?: string[];
1362
+ messages: Array<{ body: string; senderType: string; senderEmail: string; senderName: string; visibility?: string; minutesAgo: number }>
1363
+ }) {
1364
+ counter++
1365
+ const lastMsg = opts.messages[opts.messages.length - 1]
1366
+ const ticket = await ticketAdmin.create({
1367
+ subject: opts.subject, status: opts.status, priority: opts.priority,
1368
+ department: opts.department, requesterEmail: opts.requesterEmail,
1369
+ requesterName: opts.requesterName,
1370
+ ticketNumber: \`TK-\${String(counter).padStart(6, '0')}\`,
1371
+ lastReplyAt: new Date(now - lastMsg.minutesAgo * 60_000),
1372
+ lastReplierType: lastMsg.senderType,
1373
+ tags: opts.tags ?? [],
1374
+ })
1375
+ for (const msg of opts.messages) {
1376
+ await messageAdmin.create({
1377
+ ticket: ticket.id, body: msg.body, bodyFormat: 'html',
1378
+ senderType: msg.senderType, senderEmail: msg.senderEmail,
1379
+ senderName: msg.senderName, visibility: msg.visibility ?? 'public',
1380
+ })
1381
+ }
1382
+ return ticket
1383
+ }
1384
+
1385
+ await createTicket({
1386
+ subject: 'API returns 500 error on file upload',
1387
+ department: technical.id as string, status: 'open', priority: 'high',
1388
+ requesterEmail: 'john@acme.com', requesterName: 'John Smith',
1389
+ tags: [bugTag.id as string, urgentTag.id as string],
1390
+ messages: [
1391
+ { body: '<p>When I upload files larger than 5MB, I get a 500 error. Started after the last update.</p>', senderType: 'customer', senderEmail: 'john@acme.com', senderName: 'John Smith', minutesAgo: 120 },
1392
+ { body: '<p>Thanks for reporting this. I can see the error in our logs. Escalating to backend team.</p>', senderType: 'agent', senderEmail: 'sarah@support.com', senderName: 'Sarah Chen', minutesAgo: 90 },
1393
+ { body: '<p>Any update? We have a deadline tomorrow.</p>', senderType: 'customer', senderEmail: 'john@acme.com', senderName: 'John Smith', minutesAgo: 30 },
1394
+ ],
1395
+ })
1396
+
1397
+ await createTicket({
1398
+ subject: 'Invoice #2024-0847 has incorrect amount',
1399
+ department: billing.id as string, status: 'pending', priority: 'normal',
1400
+ requesterEmail: 'maria@startup.io', requesterName: 'Maria Garcia',
1401
+ messages: [
1402
+ { body: '<p>Invoice #2024-0847 shows $299 instead of the agreed $199. Please correct.</p>', senderType: 'customer', senderEmail: 'maria@startup.io', senderName: 'Maria Garcia', minutesAgo: 1440 },
1403
+ { body: '<p>Apologies for the discrepancy. Checking with billing team, should be resolved in 24h.</p>', senderType: 'agent', senderEmail: 'alex@support.com', senderName: 'Alex Johnson', minutesAgo: 1380 },
1404
+ ],
1405
+ })
1406
+
1407
+ await createTicket({
1408
+ subject: 'Production site down — 503 errors',
1409
+ department: technical.id as string, status: 'open', priority: 'urgent',
1410
+ requesterEmail: 'ops@enterprise.co', requesterName: 'Emma Wilson',
1411
+ tags: [bugTag.id as string, urgentTag.id as string],
1412
+ messages: [
1413
+ { body: '<p><strong>URGENT</strong> — Production returning 503 for all users. Need immediate help.</p>', senderType: 'customer', senderEmail: 'ops@enterprise.co', senderName: 'Emma Wilson', minutesAgo: 15 },
1414
+ ],
1415
+ })
1416
+
1417
+ await createTicket({
1418
+ subject: 'Can we get dark mode for the dashboard?',
1419
+ department: support.id as string, status: 'resolved', priority: 'low',
1420
+ requesterEmail: 'dev@techcorp.com', requesterName: 'David Lee',
1421
+ tags: [featureTag.id as string],
1422
+ messages: [
1423
+ { body: '<p>Would it be possible to add dark mode? Our team works late hours.</p>', senderType: 'customer', senderEmail: 'dev@techcorp.com', senderName: 'David Lee', minutesAgo: 10080 },
1424
+ { body: '<p>Dark mode just shipped! Toggle it in Settings → Appearance.</p>', senderType: 'agent', senderEmail: 'sarah@support.com', senderName: 'Sarah Chen', minutesAgo: 4320 },
1425
+ { body: '<p>Perfect, exactly what we needed. Thanks!</p>', senderType: 'customer', senderEmail: 'dev@techcorp.com', senderName: 'David Lee', minutesAgo: 4200 },
1426
+ ],
1427
+ })
1428
+
1429
+ return { ok: true, created: { departments: 3, tags: 5, tickets: 4, messages: 9 } }
1430
+ }
1431
+ `),await h(r(e,`app/[locale]/(shell)/admin/seed/page.tsx`),`'use client'
1432
+
1433
+ import { Button } from '@murumets-ee/ui'
1434
+ import { useState } from 'react'
1435
+ import { seedContent, seedTicketing } from './actions'
1436
+
1437
+ export default function SeedPage() {
1438
+ const [result, setResult] = useState<string | null>(null)
1439
+ const [loading, setLoading] = useState(false)
1440
+
1441
+ async function run(action: () => Promise<unknown>) {
1442
+ setLoading(true)
1443
+ setResult(null)
1444
+ try {
1445
+ const res = await action()
1446
+ setResult(JSON.stringify(res, null, 2))
1447
+ } catch (e) {
1448
+ setResult(\`Error: \${e instanceof Error ? e.message : String(e)}\`)
1449
+ } finally {
1450
+ setLoading(false)
1451
+ }
1452
+ }
1453
+
1454
+ return (
1455
+ <div className="max-w-2xl mx-auto p-8">
1456
+ <h1 className="text-2xl font-bold mb-2">Seed Data</h1>
1457
+ <p className="text-sm text-zinc-500 dark:text-zinc-400 mb-6">
1458
+ Create demo content for testing. Only available in development mode.
1459
+ </p>
1460
+
1461
+ <div className="flex gap-3 flex-wrap">
1462
+ <Button onClick={() => run(seedContent)} loading={loading}>
1463
+ Seed Content
1464
+ </Button>
1465
+ <Button onClick={() => run(seedTicketing)} loading={loading} variant="secondary">
1466
+ Seed Ticketing
1467
+ </Button>
1468
+ </div>
1469
+
1470
+ {result && (
1471
+ <pre className="mt-6 bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg p-4 text-sm overflow-auto">
1472
+ {result}
1473
+ </pre>
1474
+ )}
1475
+ </div>
1476
+ )
1477
+ }
1020
1478
  `),await h(r(e,`app/api/admin/[...path]/route.ts`),`import { createAdminApiHandler } from '@murumets-ee/admin-ui/server'
1021
1479
  import { buildPermissionChecker, buildResourceCatalog } from '@murumets-ee/auth'
1022
1480
  import { permissionRoutes } from '@murumets-ee/auth/admin'
@@ -1036,6 +1494,7 @@ import { createSettingsClient } from '@murumets-ee/settings'
1036
1494
  import { settingsRoutes } from '@murumets-ee/settings/admin'
1037
1495
  import { storageRoutes } from '@murumets-ee/storage/admin'
1038
1496
  import { taxonomyRoutes } from '@murumets-ee/taxonomy/admin'
1497
+ import { ticketingRoutes } from '@murumets-ee/ticketing/admin'
1039
1498
  import {
1040
1499
  allEntities,
1041
1500
  crudEntities,
@@ -1058,6 +1517,7 @@ const routes = [
1058
1517
  storageRoutes(),
1059
1518
  settingsRoutes(siteSettings),
1060
1519
  taxonomyRoutes(taxonomyVocabularies),
1520
+ ticketingRoutes(),
1061
1521
  logRoutes(() => new AuditLogClient(getApp().db.readWrite)),
1062
1522
  permissionRoutes({
1063
1523
  getStatements: () => buildResourceCatalog(allEntities, pluginResources),