@trackany-device/components 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +216 -0
  2. package/package.json +133 -4
  3. package/src/assets/index.ts +120 -0
  4. package/src/assets/media/avatars/300-1.png +0 -0
  5. package/src/assets/media/avatars/300-10.png +0 -0
  6. package/src/assets/media/avatars/300-11.png +0 -0
  7. package/src/assets/media/avatars/300-12.png +0 -0
  8. package/src/assets/media/avatars/300-13.png +0 -0
  9. package/src/assets/media/avatars/300-14.png +0 -0
  10. package/src/assets/media/avatars/300-15.png +0 -0
  11. package/src/assets/media/avatars/300-16.png +0 -0
  12. package/src/assets/media/avatars/300-17.png +0 -0
  13. package/src/assets/media/avatars/300-18.png +0 -0
  14. package/src/assets/media/avatars/300-19.png +0 -0
  15. package/src/assets/media/avatars/300-2.png +0 -0
  16. package/src/assets/media/avatars/300-20.png +0 -0
  17. package/src/assets/media/avatars/300-21.png +0 -0
  18. package/src/assets/media/avatars/300-22.png +0 -0
  19. package/src/assets/media/avatars/300-23.png +0 -0
  20. package/src/assets/media/avatars/300-24.png +0 -0
  21. package/src/assets/media/avatars/300-25.png +0 -0
  22. package/src/assets/media/avatars/300-26.png +0 -0
  23. package/src/assets/media/avatars/300-27.png +0 -0
  24. package/src/assets/media/avatars/300-28.png +0 -0
  25. package/src/assets/media/avatars/300-29.png +0 -0
  26. package/src/assets/media/avatars/300-3.png +0 -0
  27. package/src/assets/media/avatars/300-30.png +0 -0
  28. package/src/assets/media/avatars/300-31.png +0 -0
  29. package/src/assets/media/avatars/300-32.png +0 -0
  30. package/src/assets/media/avatars/300-33.png +0 -0
  31. package/src/assets/media/avatars/300-34.png +0 -0
  32. package/src/assets/media/avatars/300-4.png +0 -0
  33. package/src/assets/media/avatars/300-5.png +0 -0
  34. package/src/assets/media/avatars/300-6.png +0 -0
  35. package/src/assets/media/avatars/300-7.png +0 -0
  36. package/src/assets/media/avatars/300-8.png +0 -0
  37. package/src/assets/media/avatars/300-9.png +0 -0
  38. package/src/assets/media/avatars/blank.png +0 -0
  39. package/src/assets/media/avatars/gray/1.png +0 -0
  40. package/src/assets/media/avatars/gray/2.png +0 -0
  41. package/src/assets/media/avatars/gray/3.png +0 -0
  42. package/src/assets/media/avatars/gray/4.png +0 -0
  43. package/src/assets/media/avatars/gray/5.png +0 -0
  44. package/src/assets/media/illustrations/1-dark.svg +78 -0
  45. package/src/assets/media/illustrations/1.svg +78 -0
  46. package/src/assets/media/illustrations/10-dark.svg +148 -0
  47. package/src/assets/media/illustrations/10.svg +148 -0
  48. package/src/assets/media/illustrations/11-dark.svg +234 -0
  49. package/src/assets/media/illustrations/11.svg +234 -0
  50. package/src/assets/media/illustrations/12.svg +138 -0
  51. package/src/assets/media/illustrations/13.svg +205 -0
  52. package/src/assets/media/illustrations/14.svg +259 -0
  53. package/src/assets/media/illustrations/15.svg +242 -0
  54. package/src/assets/media/illustrations/16.svg +128 -0
  55. package/src/assets/media/illustrations/17.svg +180 -0
  56. package/src/assets/media/illustrations/18-dark.svg +6 -0
  57. package/src/assets/media/illustrations/18.svg +6 -0
  58. package/src/assets/media/illustrations/19-dark.svg +8 -0
  59. package/src/assets/media/illustrations/19.svg +8 -0
  60. package/src/assets/media/illustrations/2-dark.svg +78 -0
  61. package/src/assets/media/illustrations/2.svg +78 -0
  62. package/src/assets/media/illustrations/20-dark.svg +13 -0
  63. package/src/assets/media/illustrations/20.svg +13 -0
  64. package/src/assets/media/illustrations/21-dark.svg +9 -0
  65. package/src/assets/media/illustrations/21.svg +9 -0
  66. package/src/assets/media/illustrations/22-dark.svg +17 -0
  67. package/src/assets/media/illustrations/22.svg +17 -0
  68. package/src/assets/media/illustrations/23-dark.svg +13 -0
  69. package/src/assets/media/illustrations/23.svg +13 -0
  70. package/src/assets/media/illustrations/24.svg +6 -0
  71. package/src/assets/media/illustrations/25.svg +8 -0
  72. package/src/assets/media/illustrations/26.svg +8 -0
  73. package/src/assets/media/illustrations/27.svg +6 -0
  74. package/src/assets/media/illustrations/28-dark.svg +28 -0
  75. package/src/assets/media/illustrations/28.svg +14 -0
  76. package/src/assets/media/illustrations/29-dark.svg +6 -0
  77. package/src/assets/media/illustrations/29.svg +6 -0
  78. package/src/assets/media/illustrations/3-dark.svg +70 -0
  79. package/src/assets/media/illustrations/3.svg +70 -0
  80. package/src/assets/media/illustrations/30-dark.svg +8 -0
  81. package/src/assets/media/illustrations/30.svg +8 -0
  82. package/src/assets/media/illustrations/31-dark.svg +9 -0
  83. package/src/assets/media/illustrations/31.svg +9 -0
  84. package/src/assets/media/illustrations/32-dark.svg +10 -0
  85. package/src/assets/media/illustrations/32.svg +10 -0
  86. package/src/assets/media/illustrations/33-dark.svg +15 -0
  87. package/src/assets/media/illustrations/33.svg +15 -0
  88. package/src/assets/media/illustrations/34-dark.svg +5 -0
  89. package/src/assets/media/illustrations/34.svg +5 -0
  90. package/src/assets/media/illustrations/35-dark.svg +11 -0
  91. package/src/assets/media/illustrations/35.svg +4 -0
  92. package/src/assets/media/illustrations/4-dark.svg +51 -0
  93. package/src/assets/media/illustrations/4.svg +51 -0
  94. package/src/assets/media/illustrations/5-dark.svg +78 -0
  95. package/src/assets/media/illustrations/5.svg +78 -0
  96. package/src/assets/media/illustrations/6.svg +58 -0
  97. package/src/assets/media/illustrations/7.svg +49 -0
  98. package/src/assets/media/illustrations/8.svg +61 -0
  99. package/src/assets/media/illustrations/9.svg +57 -0
  100. package/src/assets/media/misc/placeholder.svg +15 -0
  101. package/src/components/devices/devices-mini-map.tsx +32 -26
  102. package/src/components/devices/map-marker.tsx +98 -0
  103. package/src/components/ui/checklist-item.tsx +55 -0
  104. package/src/components/ui/plan-card.tsx +68 -0
  105. package/src/components/ui/settings-row.tsx +32 -0
  106. package/src/components/ui/settings-section.tsx +22 -0
  107. package/src/components/ui/usage-meter.tsx +35 -0
  108. package/src/index.ts +12 -1
  109. package/src/layouts/LayoutSwitcher.tsx +220 -0
  110. package/src/layouts/app/MegaMenuLayout.tsx +69 -34
  111. package/src/layouts/app/MegaMenuNavbarLayout.tsx +73 -37
  112. package/src/layouts/app/NavbarCollapsibleLayout.tsx +53 -4
  113. package/src/layouts/app/NavbarSidebarLayout.tsx +74 -29
  114. package/src/layouts/app/SidebarDualMenuLayout.tsx +48 -5
  115. package/src/layouts/app/SidebarFixedLayout.tsx +15 -10
  116. package/src/layouts/app/SidebarMinimalLayout.tsx +51 -3
  117. package/src/layouts/app/SidebarTabsLayout.tsx +48 -2
  118. package/src/layouts/app/SplitSidebarLayout.tsx +91 -43
  119. package/src/layouts/app/TopNavLayout.tsx +7 -12
  120. package/src/layouts/app/WorkspaceSidebarLayout.tsx +103 -46
  121. package/src/layouts/app/partials/Navbar.tsx +61 -10
  122. package/src/layouts/app/partials/Toolbar.tsx +1 -1
  123. package/src/layouts/auth/AuthCenteredLayout.tsx +10 -4
  124. package/src/lib/map-markers.ts +21 -3
  125. package/src/pages/login/ConfirmPasswordPage.tsx +35 -0
  126. package/src/pages/login/ForgotPasswordPage.tsx +41 -0
  127. package/src/pages/login/LoginPage.tsx +50 -0
  128. package/src/pages/login/RegisterPage.tsx +41 -0
  129. package/src/pages/login/ResetPasswordPage.tsx +35 -0
  130. package/src/pages/login/TwoFactorChallengePage.tsx +41 -0
  131. package/src/pages/login/VerifyEmailPage.tsx +31 -0
  132. package/src/pages/my/ActivityPage.tsx +160 -0
  133. package/src/pages/my/GetStartedPage.tsx +221 -0
  134. package/src/pages/my/NotificationsPage.tsx +133 -0
  135. package/src/pages/my/ProfilePage.tsx +650 -0
  136. package/src/pages/my/TenantsPage.tsx +37 -0
  137. package/src/pages/tenant/AssigneesPage.tsx +155 -0
  138. package/src/pages/tenant/BeatsPage.tsx +403 -0
  139. package/src/pages/tenant/DashboardPage.tsx +195 -0
  140. package/src/pages/tenant/GeofencePage.tsx +422 -0
  141. package/src/pages/tenant/IncidentsPage.tsx +214 -0
  142. package/src/pages/tenant/IntegrationsPage.tsx +352 -0
  143. package/src/pages/tenant/InvitePage.tsx +153 -0
  144. package/src/pages/tenant/LiveStreamPage.tsx +141 -0
  145. package/src/pages/tenant/MembersPage.tsx +414 -0
  146. package/src/pages/tenant/TenantProfilePage.tsx +701 -0
  147. package/src/platform/adapters/default.tsx +1 -1
  148. package/src/platform/types.ts +2 -0
  149. package/src/styles/components/apexcharts.css +101 -0
  150. package/src/styles/components/image-input.css +51 -0
  151. package/src/styles/components/leaflet.css +25 -0
  152. package/src/styles/components/rating.css +89 -0
  153. package/src/styles/components/scrollable.css +119 -0
  154. package/src/styles/layout.css +24 -0
  155. package/src/styles/layouts/sidebar-fixed.css +93 -138
  156. package/src/styles/themes.css +5 -5
  157. package/src/vite-env.d.ts +21 -0
  158. package/src/layouts/SettingsLayout.tsx +0 -21
  159. package/src/layouts/app-layout.tsx +0 -29
@@ -0,0 +1,214 @@
1
+ import {
2
+ Badge, StatCard, Card, CardContent, CardHeader, CardTitle, CardDescription,
3
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
4
+ Button, Timeline, TimelineItem,
5
+ } from '@trackany-device/components';
6
+ import { LayoutResolved } from '../../layouts/LayoutSwitcher';
7
+ import type { LayoutName } from '../../layouts/LayoutSwitcher';
8
+ import { AlertTriangle, Battery, CheckCircle, Clock, Shield, Users } from 'lucide-react';
9
+ import type { LucideIcon } from 'lucide-react';
10
+ import { DevicesMiniMap } from '../../components/devices/devices-mini-map';
11
+
12
+ export type IncidentPriority = 'critical' | 'high' | 'medium' | 'low';
13
+ export type IncidentStatus = 'open' | 'acknowledged' | 'resolved';
14
+
15
+ export interface Incident {
16
+ id: string;
17
+ device: string;
18
+ assignee: string;
19
+ beat: string;
20
+ rule: string;
21
+ priority: IncidentPriority;
22
+ status: IncidentStatus;
23
+ time: string;
24
+ }
25
+
26
+ export type LogEventType = 'sos' | 'created' | 'notification' | 'assignment' | 'resolution' | 'comment';
27
+ export type TimelineVariant = 'default' | 'danger' | 'warning' | 'success' | 'info';
28
+
29
+ export interface IncidentLogEntry {
30
+ type: LogEventType;
31
+ title: string;
32
+ datetime: string;
33
+ variant?: TimelineVariant;
34
+ description: string;
35
+ }
36
+
37
+ export interface IncidentDetail {
38
+ id: string;
39
+ assignee: string;
40
+ device: string;
41
+ beat: string;
42
+ rule: string;
43
+ location: string;
44
+ reported: string;
45
+ assignedTo: string;
46
+ priority: IncidentPriority;
47
+ status: IncidentStatus;
48
+ log: IncidentLogEntry[];
49
+ lat?: number;
50
+ lng?: number;
51
+ }
52
+
53
+ const priorityStyle: Record<IncidentPriority, string> = {
54
+ critical: 'text-red-700 border-red-300 bg-red-50',
55
+ high: 'text-orange-600 border-orange-200 bg-orange-50',
56
+ medium: 'text-amber-600 border-amber-200 bg-amber-50',
57
+ low: 'text-green-600 border-green-200 bg-green-50',
58
+ };
59
+
60
+ const statusStyle: Record<IncidentStatus, string> = {
61
+ open: 'text-red-600',
62
+ acknowledged: 'text-blue-600',
63
+ resolved: 'text-green-600',
64
+ };
65
+
66
+ const LOG_ICON: Record<LogEventType, LucideIcon> = {
67
+ sos: AlertTriangle,
68
+ created: Battery,
69
+ notification: Shield,
70
+ assignment: Users,
71
+ resolution: CheckCircle,
72
+ comment: Clock,
73
+ };
74
+
75
+ export function IncidentsPage({ layout, incidents }: { layout: LayoutName; incidents: Incident[] }) {
76
+ return (
77
+ <LayoutResolved layout={layout} title="Incidents" currentUrl="/incidents">
78
+ <div className="p-6 space-y-6">
79
+ <div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
80
+ <StatCard icon={AlertTriangle} label="Open" value="6" description="2 critical" />
81
+ <StatCard icon={Clock} label="Pending" value="14" description="Needs review" />
82
+ <StatCard icon={Users} label="Assigned" value="9" description="In progress" />
83
+ <StatCard icon={CheckCircle} label="Closed today" value="23" description="↑ 8 vs yesterday" deltaType="up" delta="↑8" />
84
+ </div>
85
+ <Card>
86
+ <CardHeader className="flex flex-row items-center justify-between">
87
+ <div>
88
+ <CardTitle>Active Incidents</CardTitle>
89
+ <CardDescription>Sorted by priority · {incidents.length} of 23 shown</CardDescription>
90
+ </div>
91
+ <Button size="sm">Report incident</Button>
92
+ </CardHeader>
93
+ <CardContent className="p-0">
94
+ <Table>
95
+ <TableHeader>
96
+ <TableRow>
97
+ <TableHead>ID</TableHead>
98
+ <TableHead>Device</TableHead>
99
+ <TableHead>Assignee</TableHead>
100
+ <TableHead>Rule</TableHead>
101
+ <TableHead>Priority</TableHead>
102
+ <TableHead>Status</TableHead>
103
+ <TableHead>Time</TableHead>
104
+ </TableRow>
105
+ </TableHeader>
106
+ <TableBody>
107
+ {incidents.map((row) => (
108
+ <TableRow key={row.id}>
109
+ <TableCell className="font-mono text-xs">{row.id}</TableCell>
110
+ <TableCell className="font-mono text-xs font-medium">{row.device}</TableCell>
111
+ <TableCell>{row.assignee}</TableCell>
112
+ <TableCell>{row.rule}</TableCell>
113
+ <TableCell>
114
+ <Badge variant="outline" className={priorityStyle[row.priority]}>{row.priority}</Badge>
115
+ </TableCell>
116
+ <TableCell>
117
+ <span className={`text-xs font-medium ${statusStyle[row.status]}`}>{row.status}</span>
118
+ </TableCell>
119
+ <TableCell className="text-sm text-muted-foreground">{row.time}</TableCell>
120
+ </TableRow>
121
+ ))}
122
+ </TableBody>
123
+ </Table>
124
+ </CardContent>
125
+ </Card>
126
+ </div>
127
+ </LayoutResolved>
128
+ );
129
+ }
130
+
131
+ export function IncidentDetailPage({ layout, incident }: { layout: LayoutName; incident: IncidentDetail }) {
132
+ return (
133
+ <LayoutResolved layout={layout} title={`Incident ${incident.id}`} currentUrl={`/incidents/${incident.id}`}>
134
+ <div className="p-6 max-w-4xl space-y-6">
135
+ <div className="flex items-start justify-between gap-4">
136
+ <div>
137
+ <div className="flex items-center gap-2 mb-1">
138
+ <Badge variant="destructive" className="capitalize">{incident.priority}</Badge>
139
+ <Badge variant="outline" className="capitalize">{incident.status}</Badge>
140
+ </div>
141
+ <h1 className="text-xl font-semibold">{incident.rule} — {incident.assignee}</h1>
142
+ <p className="text-muted-foreground text-sm mt-1">{incident.id} · {incident.beat} · {incident.device}</p>
143
+ </div>
144
+ <div className="flex gap-2 shrink-0">
145
+ <Button variant="outline">Assign</Button>
146
+ <Button variant="destructive">Escalate</Button>
147
+ <Button>Resolve</Button>
148
+ </div>
149
+ </div>
150
+
151
+ <div className="grid gap-6 lg:grid-cols-3">
152
+ <div className="lg:col-span-2 space-y-4">
153
+ <Card>
154
+ <CardHeader><CardTitle>Incident details</CardTitle></CardHeader>
155
+ <CardContent className="space-y-0 text-sm">
156
+ {([
157
+ ['Assignee', incident.assignee],
158
+ ['Device', incident.device],
159
+ ['Beat', incident.beat],
160
+ ['Rule', incident.rule],
161
+ ['Location', incident.location],
162
+ ['Reported', incident.reported],
163
+ ['Assigned to', incident.assignedTo],
164
+ ] as [string, string][]).map(([label, value]) => (
165
+ <div key={label} className="flex justify-between py-2.5 border-b border-border last:border-0">
166
+ <span className="text-muted-foreground">{label}</span>
167
+ <span className="font-medium">{value}</span>
168
+ </div>
169
+ ))}
170
+ </CardContent>
171
+ </Card>
172
+ <Card>
173
+ <CardHeader><CardTitle>Location</CardTitle></CardHeader>
174
+ <CardContent className="p-0">
175
+ <DevicesMiniMap
176
+ devices={incident.lat != null && incident.lng != null ? [{
177
+ id: incident.id,
178
+ name: incident.assignee,
179
+ imei: incident.device,
180
+ last_lat: incident.lat,
181
+ last_lon: incident.lng,
182
+ signal: null,
183
+ heading: null,
184
+ }] : []}
185
+ fallbackCenter={incident.lat != null && incident.lng != null
186
+ ? { lat: incident.lat, lng: incident.lng }
187
+ : undefined}
188
+ height="192px"
189
+ className="rounded-none border-0 rounded-b-lg"
190
+ />
191
+ </CardContent>
192
+ </Card>
193
+ </div>
194
+
195
+ <Card>
196
+ <CardHeader><CardTitle>Activity log</CardTitle></CardHeader>
197
+ <CardContent>
198
+ <Timeline>
199
+ {incident.log.map((entry, i) => {
200
+ const Icon = LOG_ICON[entry.type];
201
+ return (
202
+ <TimelineItem key={i} icon={Icon} title={entry.title} datetime={entry.datetime} variant={entry.variant}>
203
+ {entry.description}
204
+ </TimelineItem>
205
+ );
206
+ })}
207
+ </Timeline>
208
+ </CardContent>
209
+ </Card>
210
+ </div>
211
+ </div>
212
+ </LayoutResolved>
213
+ );
214
+ }
@@ -0,0 +1,352 @@
1
+ import {
2
+ Badge, Button,
3
+ Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,
4
+ Input, Label,
5
+ Switch,
6
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
7
+ } from '@trackany-device/components';
8
+ import { LayoutResolved } from '../../layouts/LayoutSwitcher';
9
+ import type { LayoutName } from '../../layouts/LayoutSwitcher';
10
+ import {
11
+ AlertTriangle, Check, Copy, Eye, EyeOff, Globe, Key, Link2,
12
+ Plus, RefreshCw, Settings, Trash2, Webhook, Zap,
13
+ } from 'lucide-react';
14
+ import { useState } from 'react';
15
+
16
+ export interface Integration {
17
+ name: string;
18
+ logo: string;
19
+ desc: string;
20
+ connected: boolean;
21
+ channel: string | null;
22
+ }
23
+
24
+ export interface ApiKey {
25
+ id: number;
26
+ name: string;
27
+ key: string;
28
+ created: string;
29
+ lastUsed: string;
30
+ scopes: string[];
31
+ }
32
+
33
+ export interface WebhookEndpoint {
34
+ id: number;
35
+ url: string;
36
+ events: string[];
37
+ status: 'Active' | 'Failing';
38
+ lastPing: string;
39
+ successRate: string;
40
+ }
41
+
42
+ export interface Automation {
43
+ name: string;
44
+ trigger: string;
45
+ action: string;
46
+ enabled: boolean;
47
+ }
48
+
49
+ const ALL_EVENTS = [
50
+ 'incident.created', 'incident.resolved', 'sos.triggered',
51
+ 'device.online', 'device.offline', 'trip.started', 'trip.completed',
52
+ 'geofence.violated', 'member.invited', 'member.joined',
53
+ ];
54
+
55
+ export function ServiceIntegrationsPage({ layout, integrations }: { layout: LayoutName; integrations: Integration[] }) {
56
+ return (
57
+ <LayoutResolved layout={layout}>
58
+ <div className="p-6 max-w-4xl mx-auto space-y-6">
59
+ <div className="flex items-center justify-between">
60
+ <div>
61
+ <h1 className="text-xl font-semibold">Integrations</h1>
62
+ <p className="text-sm text-muted-foreground mt-0.5">Connect TAD with your existing tools and services.</p>
63
+ </div>
64
+ </div>
65
+
66
+ <div className="grid grid-cols-2 gap-4">
67
+ {integrations.map((intg) => (
68
+ <Card key={intg.name}>
69
+ <CardHeader className="pb-3">
70
+ <div className="flex items-start justify-between gap-3">
71
+ <div className="flex items-center gap-3">
72
+ <div className="text-2xl w-10 h-10 rounded-lg bg-muted flex items-center justify-center shrink-0">{intg.logo}</div>
73
+ <div>
74
+ <CardTitle className="text-base">{intg.name}</CardTitle>
75
+ {intg.connected && (
76
+ <div className="flex items-center gap-1 mt-0.5">
77
+ <span className="inline-block w-1.5 h-1.5 rounded-full bg-green-500" />
78
+ <span className="text-xs text-green-600">Connected{intg.channel ? ` · ${intg.channel}` : ''}</span>
79
+ </div>
80
+ )}
81
+ </div>
82
+ </div>
83
+ {intg.connected && (
84
+ <Button variant="ghost" size="icon" className="h-7 w-7 shrink-0">
85
+ <Settings className="h-3.5 w-3.5" />
86
+ </Button>
87
+ )}
88
+ </div>
89
+ </CardHeader>
90
+ <CardContent className="pb-4">
91
+ <p className="text-sm text-muted-foreground">{intg.desc}</p>
92
+ </CardContent>
93
+ <CardFooter>
94
+ {intg.connected
95
+ ? <Button variant="outline" size="sm" className="text-destructive border-destructive/30 hover:bg-destructive/5"><Trash2 className="h-3.5 w-3.5 mr-1.5" />Disconnect</Button>
96
+ : <Button variant="outline" size="sm"><Link2 className="h-3.5 w-3.5 mr-1.5" />Connect</Button>}
97
+ </CardFooter>
98
+ </Card>
99
+ ))}
100
+ </div>
101
+ </div>
102
+ </LayoutResolved>
103
+ );
104
+ }
105
+
106
+ export function ApiKeysPage({ layout, apiKeys }: { layout: LayoutName; apiKeys: ApiKey[] }) {
107
+ const [showKey, setShowKey] = useState<number | null>(null);
108
+ const [creating, setCreating] = useState(false);
109
+ const [newName, setNewName] = useState('');
110
+
111
+ return (
112
+ <LayoutResolved layout={layout}>
113
+ <div className="p-6 max-w-3xl mx-auto space-y-6">
114
+ <div className="flex items-center justify-between">
115
+ <div>
116
+ <h1 className="text-xl font-semibold">API keys</h1>
117
+ <p className="text-sm text-muted-foreground mt-0.5">Manage API keys for programmatic access to TAD data.</p>
118
+ </div>
119
+ <Button size="sm" onClick={() => setCreating(true)}><Plus className="h-4 w-4 mr-1.5" />New API key</Button>
120
+ </div>
121
+
122
+ {creating && (
123
+ <Card className="border-primary/40">
124
+ <CardHeader><CardTitle>Create API key</CardTitle></CardHeader>
125
+ <CardContent className="space-y-4">
126
+ <div className="space-y-1.5">
127
+ <Label htmlFor="key-name">Key name</Label>
128
+ <Input id="key-name" placeholder="e.g. Mobile app integration" value={newName} onChange={(e) => setNewName(e.target.value)} />
129
+ </div>
130
+ <div className="space-y-2">
131
+ <Label>Scopes</Label>
132
+ <div className="grid grid-cols-2 gap-2">
133
+ {['read:devices', 'write:devices', 'read:trips', 'read:incidents', 'write:incidents', 'read:all'].map((scope) => (
134
+ <label key={scope} className="flex items-center gap-2 text-sm cursor-pointer">
135
+ <input type="checkbox" defaultChecked={scope === 'read:devices'} className="rounded" />
136
+ <code className="text-xs bg-muted px-1.5 py-0.5 rounded">{scope}</code>
137
+ </label>
138
+ ))}
139
+ </div>
140
+ </div>
141
+ </CardContent>
142
+ <CardFooter className="justify-end gap-2">
143
+ <Button variant="outline" size="sm" onClick={() => setCreating(false)}>Cancel</Button>
144
+ <Button size="sm" onClick={() => setCreating(false)}><Key className="h-3.5 w-3.5 mr-1.5" />Create key</Button>
145
+ </CardFooter>
146
+ </Card>
147
+ )}
148
+
149
+ <div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-700 flex items-start gap-2">
150
+ <AlertTriangle className="h-4 w-4 shrink-0 mt-0.5" />
151
+ <p>API keys grant full access to your account. Never share them publicly or commit them to version control.</p>
152
+ </div>
153
+
154
+ <Card>
155
+ <CardContent className="p-0">
156
+ <Table>
157
+ <TableHeader>
158
+ <TableRow>
159
+ <TableHead>Name</TableHead>
160
+ <TableHead>Key</TableHead>
161
+ <TableHead>Scopes</TableHead>
162
+ <TableHead>Last used</TableHead>
163
+ <TableHead />
164
+ </TableRow>
165
+ </TableHeader>
166
+ <TableBody>
167
+ {apiKeys.map((k) => (
168
+ <TableRow key={k.id}>
169
+ <TableCell>
170
+ <p className="text-sm font-medium">{k.name}</p>
171
+ <p className="text-xs text-muted-foreground">Created {k.created}</p>
172
+ </TableCell>
173
+ <TableCell>
174
+ <div className="flex items-center gap-1.5">
175
+ <code className="text-xs font-mono bg-muted px-2 py-0.5 rounded">
176
+ {showKey === k.id ? k.key : `${k.key.slice(0, 16)}${'•'.repeat(12)}`}
177
+ </code>
178
+ <button onClick={() => setShowKey(showKey === k.id ? null : k.id)} className="text-muted-foreground hover:text-foreground">
179
+ {showKey === k.id ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
180
+ </button>
181
+ <button className="text-muted-foreground hover:text-foreground"><Copy className="h-3.5 w-3.5" /></button>
182
+ </div>
183
+ </TableCell>
184
+ <TableCell>
185
+ <div className="flex flex-wrap gap-1">
186
+ {k.scopes.map((s) => (
187
+ <code key={s} className="text-xs bg-muted px-1.5 py-0.5 rounded">{s}</code>
188
+ ))}
189
+ </div>
190
+ </TableCell>
191
+ <TableCell className="text-sm text-muted-foreground">{k.lastUsed}</TableCell>
192
+ <TableCell className="text-right">
193
+ <Button variant="ghost" size="sm" className="h-7 px-2 text-xs text-destructive">
194
+ <Trash2 className="h-3.5 w-3.5 mr-1" />Revoke
195
+ </Button>
196
+ </TableCell>
197
+ </TableRow>
198
+ ))}
199
+ </TableBody>
200
+ </Table>
201
+ </CardContent>
202
+ </Card>
203
+ </div>
204
+ </LayoutResolved>
205
+ );
206
+ }
207
+
208
+ export function WebhooksPage({ layout, hooks }: { layout: LayoutName; hooks: WebhookEndpoint[] }) {
209
+ const [creating, setCreating] = useState(false);
210
+
211
+ return (
212
+ <LayoutResolved layout={layout}>
213
+ <div className="p-6 max-w-3xl mx-auto space-y-6">
214
+ <div className="flex items-center justify-between">
215
+ <div>
216
+ <h1 className="text-xl font-semibold">Webhooks</h1>
217
+ <p className="text-sm text-muted-foreground mt-0.5">Receive real-time HTTP callbacks when events occur in your fleet.</p>
218
+ </div>
219
+ <Button size="sm" onClick={() => setCreating(true)}><Plus className="h-4 w-4 mr-1.5" />Add webhook</Button>
220
+ </div>
221
+
222
+ {creating && (
223
+ <Card className="border-primary/40">
224
+ <CardHeader><CardTitle>New webhook endpoint</CardTitle></CardHeader>
225
+ <CardContent className="space-y-4">
226
+ <div className="space-y-1.5">
227
+ <Label htmlFor="hook-url">Endpoint URL</Label>
228
+ <Input id="hook-url" type="url" placeholder="https://your-server.com/webhooks" />
229
+ </div>
230
+ <div className="space-y-1.5">
231
+ <Label htmlFor="hook-secret">Secret (optional)</Label>
232
+ <Input id="hook-secret" type="password" placeholder="Used to verify webhook signatures" />
233
+ </div>
234
+ <div className="space-y-2">
235
+ <Label>Events to send</Label>
236
+ <div className="grid grid-cols-2 gap-2">
237
+ {ALL_EVENTS.map((evt) => (
238
+ <label key={evt} className="flex items-center gap-2 text-sm cursor-pointer">
239
+ <input type="checkbox" className="rounded" />
240
+ <code className="text-xs bg-muted px-1.5 py-0.5 rounded">{evt}</code>
241
+ </label>
242
+ ))}
243
+ </div>
244
+ </div>
245
+ </CardContent>
246
+ <CardFooter className="justify-end gap-2">
247
+ <Button variant="outline" size="sm" onClick={() => setCreating(false)}>Cancel</Button>
248
+ <Button size="sm" onClick={() => setCreating(false)}><Webhook className="h-3.5 w-3.5 mr-1.5" />Save endpoint</Button>
249
+ </CardFooter>
250
+ </Card>
251
+ )}
252
+
253
+ <div className="space-y-4">
254
+ {hooks.map((hook) => (
255
+ <Card key={hook.id} className={hook.status === 'Failing' ? 'border-red-200' : ''}>
256
+ <CardHeader className="pb-2">
257
+ <div className="flex items-start justify-between gap-3">
258
+ <div className="flex-1 min-w-0">
259
+ <div className="flex items-center gap-2">
260
+ <Badge variant="outline" className={`text-xs shrink-0 ${hook.status === 'Active' ? 'text-green-600 border-green-200 bg-green-50' : 'text-red-600 border-red-200 bg-red-50'}`}>
261
+ {hook.status}
262
+ </Badge>
263
+ <code className="text-xs font-mono text-muted-foreground truncate">{hook.url}</code>
264
+ <button className="text-muted-foreground hover:text-foreground shrink-0"><Copy className="h-3.5 w-3.5" /></button>
265
+ </div>
266
+ </div>
267
+ <div className="flex items-center gap-1 shrink-0">
268
+ <Button variant="ghost" size="icon" className="h-7 w-7"><RefreshCw className="h-3.5 w-3.5" /></Button>
269
+ <Button variant="ghost" size="icon" className="h-7 w-7"><Settings className="h-3.5 w-3.5" /></Button>
270
+ <Button variant="ghost" size="icon" className="h-7 w-7 text-destructive"><Trash2 className="h-3.5 w-3.5" /></Button>
271
+ </div>
272
+ </div>
273
+ </CardHeader>
274
+ <CardContent className="pb-4">
275
+ <div className="flex items-center gap-4 text-xs text-muted-foreground mb-3">
276
+ <span>Last ping: <strong className="text-foreground">{hook.lastPing}</strong></span>
277
+ <span>Success rate: <strong className={hook.status === 'Failing' ? 'text-red-600' : 'text-foreground'}>{hook.successRate}</strong></span>
278
+ </div>
279
+ <div className="flex flex-wrap gap-1">
280
+ {hook.events.map((evt) => (
281
+ <code key={evt} className="text-xs bg-muted px-1.5 py-0.5 rounded">{evt}</code>
282
+ ))}
283
+ </div>
284
+ </CardContent>
285
+ </Card>
286
+ ))}
287
+ </div>
288
+
289
+ <Card>
290
+ <CardHeader>
291
+ <CardTitle>Webhook documentation</CardTitle>
292
+ <CardDescription>All payloads are signed with HMAC-SHA256. Verify the <code className="text-xs bg-muted px-1 py-0.5 rounded">X-TAD-Signature</code> header on your server.</CardDescription>
293
+ </CardHeader>
294
+ <CardContent>
295
+ <Button variant="outline" size="sm"><Globe className="h-3.5 w-3.5 mr-1.5" />View webhook docs</Button>
296
+ </CardContent>
297
+ </Card>
298
+ </div>
299
+ </LayoutResolved>
300
+ );
301
+ }
302
+
303
+ export function AutomationsPage({ layout, automations }: { layout: LayoutName; automations: Automation[] }) {
304
+ return (
305
+ <LayoutResolved layout={layout}>
306
+ <div className="p-6 max-w-3xl mx-auto space-y-6">
307
+ <div className="flex items-center justify-between">
308
+ <div>
309
+ <h1 className="text-xl font-semibold">Automations</h1>
310
+ <p className="text-sm text-muted-foreground mt-0.5">Automate actions between TAD and third-party services.</p>
311
+ </div>
312
+ <Button size="sm"><Zap className="h-4 w-4 mr-1.5" />New automation</Button>
313
+ </div>
314
+
315
+ <Card>
316
+ <CardContent className="p-0">
317
+ {automations.map((zap, i) => (
318
+ <div key={i} className="flex items-center justify-between gap-4 px-5 py-4 border-b border-border last:border-0">
319
+ <div className="flex items-center gap-3 flex-1 min-w-0">
320
+ <Zap className={`h-4 w-4 shrink-0 ${zap.enabled ? 'text-amber-500' : 'text-muted-foreground/40'}`} />
321
+ <div className="min-w-0">
322
+ <p className="text-sm font-medium truncate">{zap.name}</p>
323
+ <div className="flex items-center gap-1.5 mt-0.5">
324
+ <code className="text-xs bg-muted px-1.5 py-0.5 rounded">{zap.trigger}</code>
325
+ <span className="text-muted-foreground">→</span>
326
+ <span className="text-xs text-muted-foreground">{zap.action}</span>
327
+ </div>
328
+ </div>
329
+ </div>
330
+ <div className="flex items-center gap-2 shrink-0">
331
+ <Button variant="ghost" size="icon" className="h-7 w-7"><Settings className="h-3.5 w-3.5" /></Button>
332
+ <Switch defaultChecked={zap.enabled} />
333
+ </div>
334
+ </div>
335
+ ))}
336
+ </CardContent>
337
+ </Card>
338
+
339
+ <Card>
340
+ <CardHeader>
341
+ <CardTitle>Connect Zapier</CardTitle>
342
+ <CardDescription>Zapier connects TAD to 5,000+ apps without code.</CardDescription>
343
+ </CardHeader>
344
+ <CardContent className="flex items-center gap-3">
345
+ <Button variant="outline" size="sm"><Zap className="h-3.5 w-3.5 mr-1.5" />Connect Zapier</Button>
346
+ <Button variant="outline" size="sm"><Globe className="h-3.5 w-3.5 mr-1.5" />Browse templates</Button>
347
+ </CardContent>
348
+ </Card>
349
+ </div>
350
+ </LayoutResolved>
351
+ );
352
+ }