@lego-box/shell 1.0.5 → 1.0.7

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.
Files changed (49) hide show
  1. package/.krasrc +13 -0
  2. package/dist/emulator/lego-box-shell-1.0.7.tgz +0 -0
  3. package/package.json +6 -3
  4. package/postcss.config.js +6 -0
  5. package/src/auth/auth-store.ts +33 -0
  6. package/src/auth/auth.ts +176 -0
  7. package/src/components/ProtectedPage.tsx +48 -0
  8. package/src/config/env.node.ts +38 -0
  9. package/src/config/env.ts +105 -0
  10. package/src/context/AbilityContext.tsx +213 -0
  11. package/src/context/PiralInstanceContext.tsx +17 -0
  12. package/src/hooks/index.ts +11 -0
  13. package/src/hooks/useAuditLogs.ts +190 -0
  14. package/src/hooks/useDebounce.ts +34 -0
  15. package/src/hooks/usePermissionGuard.tsx +39 -0
  16. package/src/hooks/usePermissions.ts +190 -0
  17. package/src/hooks/useRoles.ts +233 -0
  18. package/src/hooks/useTickets.ts +214 -0
  19. package/src/hooks/useUserLogins.ts +39 -0
  20. package/src/hooks/useUsers.ts +252 -0
  21. package/src/index.html +16 -0
  22. package/src/index.tsx +296 -0
  23. package/src/layout.tsx +246 -0
  24. package/src/migrations/config.ts +62 -0
  25. package/src/migrations/dev-migrations.ts +75 -0
  26. package/src/migrations/index.ts +13 -0
  27. package/src/migrations/run-migrations.ts +187 -0
  28. package/src/migrations/runner.ts +925 -0
  29. package/src/migrations/types.ts +207 -0
  30. package/src/migrations/utils.ts +264 -0
  31. package/src/pages/AuditLogsPage.tsx +378 -0
  32. package/src/pages/ContactSupportPage.tsx +610 -0
  33. package/src/pages/LandingPage.tsx +221 -0
  34. package/src/pages/LoginPage.tsx +217 -0
  35. package/src/pages/MigrationsPage.tsx +1364 -0
  36. package/src/pages/ProfilePage.tsx +335 -0
  37. package/src/pages/SettingsPage.tsx +101 -0
  38. package/src/pages/SystemHealthCheckPage.tsx +144 -0
  39. package/src/pages/UserManagementPage.tsx +1010 -0
  40. package/src/piral/api.ts +39 -0
  41. package/src/piral/auth-casl.ts +56 -0
  42. package/src/piral/menu.ts +102 -0
  43. package/src/piral/piral.json +4 -0
  44. package/src/services/telemetry.ts +84 -0
  45. package/src/styles/globals.css +1351 -0
  46. package/src/utils/auditLogger.ts +68 -0
  47. package/tailwind.config.js +86 -0
  48. package/webpack.config.js +89 -0
  49. package/dist/emulator/lego-box-shell-1.0.5.tgz +0 -0
@@ -0,0 +1,610 @@
1
+ import * as React from 'react';
2
+ import {
3
+ QuickActionCard,
4
+ TicketStatsCard,
5
+ TicketFormDialog,
6
+ TicketFormData,
7
+ DataTable,
8
+ DataTableColumn,
9
+ SearchInput,
10
+ FilterChip,
11
+ Button,
12
+ DeleteConfirmationDialog,
13
+ Pagination,
14
+ Badge,
15
+ } from '@lego-box/ui-kit';
16
+ import { useTickets } from '../hooks/useTickets';
17
+ import { useDebounce } from '../hooks';
18
+ import { Ticket, TicketCategory, TicketPriority, TicketStatus } from '../types';
19
+ import {
20
+ Bug,
21
+ Lightbulb,
22
+ MessageCircle,
23
+ Ticket as TicketIcon,
24
+ Timer,
25
+ CircleCheck,
26
+ Clock,
27
+ TrendingUp,
28
+ AlertCircle,
29
+ Check,
30
+ Zap,
31
+ Plus,
32
+ Search,
33
+ Loader2
34
+ } from 'lucide-react';
35
+
36
+ /**
37
+ * Get badge variant based on ticket status
38
+ */
39
+ function getStatusBadgeVariant(status: TicketStatus) {
40
+ switch (status) {
41
+ case 'open':
42
+ return 'default';
43
+ case 'in_progress':
44
+ return 'warning';
45
+ case 'resolved':
46
+ return 'success';
47
+ case 'closed':
48
+ return 'secondary';
49
+ default:
50
+ return 'default';
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Get badge variant based on ticket priority
56
+ */
57
+ function getPriorityBadgeVariant(priority: TicketPriority) {
58
+ switch (priority) {
59
+ case 'high':
60
+ return 'destructive';
61
+ case 'medium':
62
+ return 'warning';
63
+ case 'low':
64
+ return 'secondary';
65
+ default:
66
+ return 'default';
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Get badge variant based on ticket category
72
+ */
73
+ function getCategoryBadgeVariant(category: TicketCategory) {
74
+ switch (category) {
75
+ case 'bug':
76
+ return 'destructive';
77
+ case 'feature':
78
+ return 'info';
79
+ case 'inquiry':
80
+ return 'secondary';
81
+ default:
82
+ return 'default';
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Format date to human-readable format
88
+ */
89
+ function formatDate(dateString: string): string {
90
+ const date = new Date(dateString);
91
+ return date.toLocaleDateString('en-US', {
92
+ month: 'short',
93
+ day: 'numeric',
94
+ year: 'numeric',
95
+ });
96
+ }
97
+
98
+ /**
99
+ * Contact Support Page
100
+ *
101
+ * Features:
102
+ * - Quick action cards for creating tickets
103
+ * - Stats overview cards
104
+ * - Search and filter functionality
105
+ * - Data table with ticket information
106
+ * - CRUD operations with shared PocketBase
107
+ */
108
+ export function ContactSupportPage() {
109
+ // Search and filter state
110
+ const [searchQuery, setSearchQuery] = React.useState('');
111
+ const [statusFilter, setStatusFilter] = React.useState<TicketStatus | 'all'>('all');
112
+ const [categoryFilter, setCategoryFilter] = React.useState<TicketCategory | 'all'>('all');
113
+ const [priorityFilter, setPriorityFilter] = React.useState<TicketPriority | 'all'>('all');
114
+ const [page, setPage] = React.useState(1);
115
+ const [perPage, setPerPage] = React.useState(25);
116
+
117
+ // Dialog state
118
+ const [ticketFormOpen, setTicketFormOpen] = React.useState(false);
119
+ const [deleteDialogOpen, setDeleteDialogOpen] = React.useState(false);
120
+ const [selectedTicket, setSelectedTicket] = React.useState<Ticket | null>(null);
121
+ const [initialCategory, setInitialCategory] = React.useState<TicketCategory | undefined>(undefined);
122
+ const [formLoading, setFormLoading] = React.useState(false);
123
+
124
+ // Debounced search query
125
+ const debouncedSearchQuery = useDebounce(searchQuery, 500);
126
+
127
+ // Fetch tickets with filters
128
+ const {
129
+ tickets,
130
+ loading,
131
+ error,
132
+ pagination,
133
+ refetch,
134
+ createTicket,
135
+ updateTicket,
136
+ deleteTicket,
137
+ } = useTickets({
138
+ page,
139
+ perPage,
140
+ searchQuery: debouncedSearchQuery,
141
+ statusFilter,
142
+ categoryFilter,
143
+ priorityFilter,
144
+ });
145
+
146
+ // Calculate stats
147
+ const stats = React.useMemo(() => {
148
+ const total = pagination?.totalItems || 0;
149
+ const open = tickets.filter((t) => t.status === 'open').length;
150
+ const resolved = tickets.filter((t) => t.status === 'resolved').length;
151
+ const inProgress = tickets.filter((t) => t.status === 'in_progress').length;
152
+
153
+ return { total, open, resolved, inProgress };
154
+ }, [tickets, pagination?.totalItems]);
155
+
156
+ // Handle quick action click
157
+ const handleQuickActionClick = (category: TicketCategory) => {
158
+ setInitialCategory(category);
159
+ setSelectedTicket(null);
160
+ setTicketFormOpen(true);
161
+ };
162
+
163
+ // Handle create ticket
164
+ const handleCreateTicket = async (formData: TicketFormData) => {
165
+ setFormLoading(true);
166
+ try {
167
+ await createTicket({
168
+ title: formData.title,
169
+ description: formData.description,
170
+ category: formData.category,
171
+ priority: formData.priority,
172
+ status: formData.status,
173
+ });
174
+ setTicketFormOpen(false);
175
+ refetch();
176
+ } catch (err: any) {
177
+ throw err;
178
+ } finally {
179
+ setFormLoading(false);
180
+ }
181
+ };
182
+
183
+ // Handle update ticket
184
+ const handleUpdateTicket = async (formData: TicketFormData) => {
185
+ if (!selectedTicket) return;
186
+ setFormLoading(true);
187
+ try {
188
+ await updateTicket(selectedTicket.id, {
189
+ title: formData.title,
190
+ description: formData.description,
191
+ category: formData.category,
192
+ priority: formData.priority,
193
+ status: formData.status,
194
+ });
195
+ setTicketFormOpen(false);
196
+ setSelectedTicket(null);
197
+ refetch();
198
+ } catch (err: any) {
199
+ throw err;
200
+ } finally {
201
+ setFormLoading(false);
202
+ }
203
+ };
204
+
205
+ // Handle delete ticket
206
+ const handleDeleteTicket = async () => {
207
+ if (!selectedTicket) return;
208
+ setFormLoading(true);
209
+ try {
210
+ await deleteTicket(selectedTicket.id);
211
+ setDeleteDialogOpen(false);
212
+ setSelectedTicket(null);
213
+ refetch();
214
+ } catch (err: any) {
215
+ throw err;
216
+ } finally {
217
+ setFormLoading(false);
218
+ }
219
+ };
220
+
221
+ // Handle edit click
222
+ const handleEditClick = (ticket: Ticket) => {
223
+ setSelectedTicket(ticket);
224
+ setInitialCategory(undefined);
225
+ setTicketFormOpen(true);
226
+ };
227
+
228
+ // Handle delete click
229
+ const handleDeleteClick = (ticket: Ticket) => {
230
+ setSelectedTicket(ticket);
231
+ setDeleteDialogOpen(true);
232
+ };
233
+
234
+ // Ticket table columns - matching Pencil design
235
+ const ticketColumns: DataTableColumn<Ticket>[] = [
236
+ {
237
+ key: 'id',
238
+ header: 'ID',
239
+ width: '8%',
240
+ cell: (ticket: Ticket) => (
241
+ <span className="text-xs font-medium text-blue-600">#{ticket.id.slice(-6).toUpperCase()}</span>
242
+ ),
243
+ },
244
+ {
245
+ key: 'subject',
246
+ header: 'Subject',
247
+ width: '32%',
248
+ cell: (ticket: Ticket) => (
249
+ <div className="flex items-center gap-2">
250
+ {ticket.category === 'bug' && <Bug className="w-4 h-4 text-red-500" />}
251
+ {ticket.category === 'feature' && <Lightbulb className="w-4 h-4 text-blue-500" />}
252
+ {ticket.category === 'inquiry' && <MessageCircle className="w-4 h-4 text-purple-500" />}
253
+ <span className="text-sm text-foreground truncate">{ticket.title}</span>
254
+ </div>
255
+ ),
256
+ },
257
+ {
258
+ key: 'category',
259
+ header: 'Category',
260
+ width: '12%',
261
+ cell: (ticket: Ticket) => (
262
+ <Badge variant={getCategoryBadgeVariant(ticket.category)}>
263
+ {ticket.category.charAt(0).toUpperCase() + ticket.category.slice(1)}
264
+ </Badge>
265
+ ),
266
+ },
267
+ {
268
+ key: 'status',
269
+ header: 'Status',
270
+ width: '12%',
271
+ cell: (ticket: Ticket) => (
272
+ <Badge variant={getStatusBadgeVariant(ticket.status)}>
273
+ {ticket.status.replace('_', ' ').replace(/\b\w/g, (l) => l.toUpperCase())}
274
+ </Badge>
275
+ ),
276
+ },
277
+ {
278
+ key: 'priority',
279
+ header: 'Priority',
280
+ width: '10%',
281
+ cell: (ticket: Ticket) => (
282
+ <Badge variant={getPriorityBadgeVariant(ticket.priority)}>
283
+ {ticket.priority.charAt(0).toUpperCase() + ticket.priority.slice(1)}
284
+ </Badge>
285
+ ),
286
+ },
287
+ {
288
+ key: 'created',
289
+ header: 'Created',
290
+ width: '14%',
291
+ cell: (ticket: Ticket) => (
292
+ <span className="text-sm text-muted-foreground">{formatDate(ticket.created)}</span>
293
+ ),
294
+ },
295
+ {
296
+ key: 'actions',
297
+ header: 'Actions',
298
+ width: '12%',
299
+ cell: (ticket: Ticket) => (
300
+ <div className="flex items-center gap-3">
301
+ <button
302
+ onClick={() => handleEditClick(ticket)}
303
+ className="text-sm font-medium text-blue-600 hover:text-blue-700 transition-colors"
304
+ >
305
+ Edit
306
+ </button>
307
+ <button
308
+ onClick={() => handleDeleteClick(ticket)}
309
+ className="text-sm font-medium text-red-500 hover:text-red-600 transition-colors"
310
+ >
311
+ Delete
312
+ </button>
313
+ </div>
314
+ ),
315
+ },
316
+ ];
317
+
318
+ return (
319
+ <div className="min-h-screen bg-background">
320
+ <div className="p-6 space-y-6">
321
+ {/* Page Header */}
322
+ <div className="flex items-center justify-between">
323
+ <div>
324
+ <h1 className="text-2xl font-bold text-foreground">Contact Support</h1>
325
+ <p className="text-sm text-muted-foreground mt-1">
326
+ Get help with bug reports, feature requests, and general inquiries
327
+ </p>
328
+ </div>
329
+ <div className="flex items-center gap-3">
330
+ <div className="relative">
331
+ <Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
332
+ <input
333
+ type="text"
334
+ placeholder="Search Tickets"
335
+ value={searchQuery}
336
+ onChange={(e) => setSearchQuery(e.target.value)}
337
+ className="h-10 pl-10 pr-4 rounded-lg border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 w-48"
338
+ />
339
+ </div>
340
+ <Button
341
+ className="gap-2 bg-blue-600 hover:bg-blue-700"
342
+ onClick={() => {
343
+ setInitialCategory(undefined);
344
+ setSelectedTicket(null);
345
+ setTicketFormOpen(true);
346
+ }}
347
+ >
348
+ <Plus className="w-4 h-4" />
349
+ New Ticket
350
+ </Button>
351
+ </div>
352
+ </div>
353
+
354
+ {/* Quick Actions - Horizontal layout matching Pencil */}
355
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
356
+ <QuickActionCard
357
+ title="Report a Bug"
358
+ description="Found an issue? Let us know."
359
+ icon={<Bug className="w-full h-full" />}
360
+ variant="bug"
361
+ onClick={() => handleQuickActionClick('bug')}
362
+ />
363
+ <QuickActionCard
364
+ title="Request a Feature"
365
+ description="Share your ideas with us."
366
+ icon={<Lightbulb className="w-full h-full" />}
367
+ variant="feature"
368
+ onClick={() => handleQuickActionClick('feature')}
369
+ />
370
+ <QuickActionCard
371
+ title="General Inquiry"
372
+ description="Have questions? We're here."
373
+ icon={<MessageCircle className="w-full h-full" />}
374
+ variant="inquiry"
375
+ onClick={() => handleQuickActionClick('inquiry')}
376
+ />
377
+ </div>
378
+
379
+ {/* Stats Grid - Compact horizontal layout matching Pencil */}
380
+ <div className="grid" style={{ gridTemplateColumns: 'repeat(4, 1fr)', gap: '1rem' }}>
381
+ <TicketStatsCard
382
+ value={stats.total}
383
+ label="Total Tickets"
384
+ icon={<TicketIcon className="w-full h-full" />}
385
+ iconBgColor="bg-blue-50"
386
+ iconColor="text-blue-600"
387
+ badge={{
388
+ icon: <TrendingUp className="w-3 h-3" />,
389
+ text: '+8%',
390
+ bgColor: 'bg-green-50',
391
+ textColor: 'text-green-600',
392
+ }}
393
+ />
394
+ <TicketStatsCard
395
+ value={stats.open}
396
+ label="Open Tickets"
397
+ icon={<Timer className="w-full h-full" />}
398
+ iconBgColor="bg-amber-50"
399
+ iconColor="text-amber-600"
400
+ badge={{
401
+ icon: <AlertCircle className="w-3 h-3" />,
402
+ text: `${stats.inProgress} in progress`,
403
+ bgColor: 'bg-amber-50',
404
+ textColor: 'text-amber-600',
405
+ }}
406
+ />
407
+ <TicketStatsCard
408
+ value={stats.resolved}
409
+ label="Resolved"
410
+ icon={<CircleCheck className="w-full h-full" />}
411
+ iconBgColor="bg-green-50"
412
+ iconColor="text-green-600"
413
+ badge={{
414
+ icon: <Check className="w-3 h-3" />,
415
+ text: '94% rate',
416
+ bgColor: 'bg-green-50',
417
+ textColor: 'text-green-600',
418
+ }}
419
+ />
420
+ <TicketStatsCard
421
+ value="4h 32m"
422
+ label="Avg Response Time"
423
+ icon={<Clock className="w-full h-full" />}
424
+ iconBgColor="bg-purple-50"
425
+ iconColor="text-purple-600"
426
+ badge={{
427
+ icon: <Zap className="w-3 h-3" />,
428
+ text: 'Fast',
429
+ bgColor: 'bg-purple-50',
430
+ textColor: 'text-purple-600',
431
+ }}
432
+ />
433
+ </div>
434
+
435
+ {/* Filters and Search - Dropdown style matching Pencil */}
436
+ <div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
437
+ <div className="flex flex-wrap gap-3">
438
+ {/* Status Filter Dropdown */}
439
+ <div>
440
+ <label className="text-xs text-muted-foreground mb-1 block">Status:</label>
441
+ <div className="relative">
442
+ <select
443
+ value={statusFilter}
444
+ onChange={(e) => setStatusFilter(e.target.value as TicketStatus | 'all')}
445
+ className="h-9 px-3 pr-8 rounded-lg border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 appearance-none cursor-pointer min-w-[120px] w-full"
446
+ >
447
+ <option value="all">All Tickets</option>
448
+ <option value="open">Open</option>
449
+ <option value="in_progress">In Progress</option>
450
+ <option value="resolved">Resolved</option>
451
+ <option value="closed">Closed</option>
452
+ </select>
453
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
454
+ <svg className="w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
455
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
456
+ </svg>
457
+ </div>
458
+ </div>
459
+ </div>
460
+
461
+ {/* Category Filter Dropdown */}
462
+ <div>
463
+ <label className="text-xs text-muted-foreground mb-1 block">Category:</label>
464
+ <div className="relative">
465
+ <select
466
+ value={categoryFilter}
467
+ onChange={(e) => setCategoryFilter(e.target.value as TicketCategory | 'all')}
468
+ className="h-9 px-3 pr-8 rounded-lg border border-border bg-background text-sm focus:outline-none focus:ring-2 focus:ring-primary/20 appearance-none cursor-pointer min-w-[120px] w-full"
469
+ >
470
+ <option value="all">All Categories</option>
471
+ <option value="bug">Bug Report</option>
472
+ <option value="feature">Feature Request</option>
473
+ <option value="inquiry">General Inquiry</option>
474
+ </select>
475
+ <div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
476
+ <svg className="w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
477
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
478
+ </svg>
479
+ </div>
480
+ </div>
481
+ </div>
482
+ </div>
483
+ </div>
484
+
485
+ {/* Tickets Table */}
486
+ <div className="bg-card rounded-lg border border-border shadow-sm">
487
+ {loading ? (
488
+ <div className="flex items-center justify-center p-12">
489
+ <Loader2 className="w-8 h-8 animate-spin text-primary" />
490
+ <span className="ml-2 text-muted-foreground">Loading tickets...</span>
491
+ </div>
492
+ ) : tickets.length === 0 ? (
493
+ <div className="flex flex-col items-center justify-center min-h-[360px] py-24 px-16 text-center">
494
+ <TicketIcon className="w-12 h-12 text-muted-foreground mb-6" />
495
+ <h3 className="text-lg font-medium text-foreground mb-3">No tickets found</h3>
496
+ <p className="text-sm text-muted-foreground mb-6 max-w-sm">
497
+ Create your first ticket to get started with support.
498
+ </p>
499
+ <Button
500
+ onClick={() => {
501
+ setInitialCategory(undefined);
502
+ setSelectedTicket(null);
503
+ setTicketFormOpen(true);
504
+ }}
505
+ >
506
+ <Plus className="w-4 h-4 mr-2" />
507
+ Create Ticket
508
+ </Button>
509
+ </div>
510
+ ) : (
511
+ <>
512
+ <DataTable
513
+ data={tickets}
514
+ columns={ticketColumns}
515
+ keyExtractor={(ticket) => ticket.id}
516
+ variant="inline"
517
+ />
518
+
519
+ {/* Pagination - Matching Pencil design */}
520
+ {pagination && (
521
+ <div className="p-4 border-t flex items-center justify-between">
522
+ <span className="text-sm text-muted-foreground">
523
+ Showing {((pagination.page - 1) * pagination.perPage) + 1}-{Math.min(pagination.page * pagination.perPage, pagination.totalItems)} of {pagination.totalItems} tickets
524
+ </span>
525
+
526
+ {pagination.totalPages > 1 && (
527
+ <div className="flex items-center gap-1">
528
+ {/* Page size selector */}
529
+ <select
530
+ value={perPage}
531
+ onChange={(e) => setPerPage(Number(e.target.value))}
532
+ className="h-8 px-2 rounded border border-border text-sm mr-4"
533
+ >
534
+ <option value={10}>10 / page</option>
535
+ <option value={25}>25 / page</option>
536
+ <option value={50}>50 / page</option>
537
+ </select>
538
+
539
+ {/* Page numbers */}
540
+ {Array.from({ length: Math.min(5, pagination.totalPages) }, (_, i) => {
541
+ const pageNum = i + 1;
542
+ return (
543
+ <button
544
+ key={pageNum}
545
+ onClick={() => setPage(pageNum)}
546
+ className={`w-8 h-8 rounded text-sm font-medium transition-colors ${
547
+ page === pageNum
548
+ ? 'bg-blue-600 text-white'
549
+ : 'text-muted-foreground hover:bg-muted'
550
+ }`}
551
+ >
552
+ {pageNum}
553
+ </button>
554
+ );
555
+ })}
556
+
557
+ {pagination.totalPages > 5 && (
558
+ <>
559
+ <span className="text-muted-foreground px-1">...</span>
560
+ <button
561
+ onClick={() => setPage(pagination.totalPages)}
562
+ className={`w-8 h-8 rounded text-sm font-medium transition-colors ${
563
+ page === pagination.totalPages
564
+ ? 'bg-blue-600 text-white'
565
+ : 'text-muted-foreground hover:bg-muted'
566
+ }`}
567
+ >
568
+ {pagination.totalPages}
569
+ </button>
570
+ </>
571
+ )}
572
+ </div>
573
+ )}
574
+ </div>
575
+ )}
576
+ </>
577
+ )}
578
+ </div>
579
+
580
+ {/* Ticket Form Dialog */}
581
+ <TicketFormDialog
582
+ isOpen={ticketFormOpen}
583
+ onClose={() => {
584
+ setTicketFormOpen(false);
585
+ setSelectedTicket(null);
586
+ setInitialCategory(undefined);
587
+ }}
588
+ onSubmit={selectedTicket ? handleUpdateTicket : handleCreateTicket}
589
+ ticket={selectedTicket}
590
+ loading={formLoading}
591
+ initialCategory={initialCategory}
592
+ />
593
+
594
+ {/* Delete Confirmation Dialog */}
595
+ <DeleteConfirmationDialog
596
+ isOpen={deleteDialogOpen}
597
+ onClose={() => {
598
+ setDeleteDialogOpen(false);
599
+ setSelectedTicket(null);
600
+ }}
601
+ onConfirm={handleDeleteTicket}
602
+ title="Delete Ticket"
603
+ description={`Are you sure you want to delete the ticket "${selectedTicket?.title}"? This action cannot be undone.`}
604
+ itemName={selectedTicket?.title || ''}
605
+ loading={formLoading}
606
+ />
607
+ </div>
608
+ </div>
609
+ );
610
+ }