@trackany-device/components 1.1.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 +9 -9
  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,701 @@
1
+ import {
2
+ Avatar, AvatarFallback,
3
+ Badge,
4
+ Button,
5
+ Card, CardContent, CardDescription, CardHeader, CardTitle,
6
+ Checkbox,
7
+ Input, Label, Textarea,
8
+ PasswordInput,
9
+ PlanCard,
10
+ RadioGroup,
11
+ Select,
12
+ Separator,
13
+ Switch,
14
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
15
+ Tabs, TabsContent, TabsList, TabsTrigger,
16
+ AppearanceTabs,
17
+ } from '@trackany-device/components';
18
+ import { LayoutResolved } from '../../layouts/LayoutSwitcher';
19
+ import type { LayoutName } from '../../layouts/LayoutSwitcher';
20
+ import {
21
+ AlertTriangle, Bell, Building2, Camera, CheckCircle2, ChevronRight,
22
+ Copy, CreditCard, Download, KeyRound, Laptop,
23
+ Link2, Lock, Mail, Palette, Plus,
24
+ Shield, ShieldCheck, Smartphone, Star, Trash2, UserCircle, UserCog, Users,
25
+ } from 'lucide-react';
26
+ import { useState } from 'react';
27
+
28
+ export interface TenantUser {
29
+ name: string;
30
+ email: string;
31
+ phone: string;
32
+ role: string;
33
+ initials: string;
34
+ }
35
+
36
+ export interface TenantOrg {
37
+ name: string;
38
+ slug: string;
39
+ email: string;
40
+ phone: string;
41
+ vat: string;
42
+ reg: string;
43
+ website: string;
44
+ }
45
+
46
+ export interface OrgMember {
47
+ name: string;
48
+ email: string;
49
+ role: string;
50
+ twofa: boolean;
51
+ joined: string;
52
+ initials: string;
53
+ }
54
+
55
+ export interface SecurityLogEntry {
56
+ event: string;
57
+ user: string;
58
+ ip: string;
59
+ date: string;
60
+ ok: boolean;
61
+ }
62
+
63
+ export interface TenantInvoice {
64
+ id: string;
65
+ date: string;
66
+ amount: string;
67
+ status: string;
68
+ }
69
+
70
+ const SIDEBAR_NAV = [
71
+ { id: 'info', label: 'Personal info', icon: UserCircle },
72
+ { id: 'auth', label: 'Email & password', icon: Mail },
73
+ { id: 'twofa', label: '2FA', icon: ShieldCheck },
74
+ { id: 'social', label: 'Social sign-in', icon: Link2 },
75
+ { id: 'notif', label: 'Notifications', icon: Bell },
76
+ { id: 'appear', label: 'Appearance', icon: Palette },
77
+ { id: 'prefs', label: 'Preferences', icon: UserCog },
78
+ { id: 'api', label: 'API access', icon: KeyRound },
79
+ { id: 'delete', label: 'Danger zone', icon: Trash2 },
80
+ ];
81
+
82
+ export function UserProfileTabsPage({ layout, user }: { layout: LayoutName; user: TenantUser }) {
83
+ const [name, setName] = useState(user.name);
84
+ return (
85
+ <LayoutResolved layout={layout} title="My Profile" currentUrl="/profile">
86
+ <div className="p-6 max-w-2xl space-y-6">
87
+ <div className="flex items-center gap-4">
88
+ <div className="relative shrink-0">
89
+ <Avatar className="h-16 w-16"><AvatarFallback className="text-xl">{user.initials}</AvatarFallback></Avatar>
90
+ <button className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full bg-primary text-primary-foreground shadow-sm">
91
+ <Camera className="h-3.5 w-3.5" />
92
+ </button>
93
+ </div>
94
+ <div>
95
+ <h1 className="text-xl font-semibold">{name}</h1>
96
+ <p className="text-sm text-muted-foreground">{user.email}</p>
97
+ <div className="flex gap-1.5 mt-1">
98
+ <Badge variant="outline">{user.role}</Badge>
99
+ <Badge variant="outline" className="text-green-600 border-green-200 bg-green-50">Active</Badge>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <Tabs defaultValue="profile">
105
+ <TabsList>
106
+ <TabsTrigger value="profile"><UserCircle className="h-4 w-4 mr-1.5" />Profile</TabsTrigger>
107
+ <TabsTrigger value="password"><Lock className="h-4 w-4 mr-1.5" />Password</TabsTrigger>
108
+ <TabsTrigger value="notifications"><Bell className="h-4 w-4 mr-1.5" />Notifications</TabsTrigger>
109
+ <TabsTrigger value="security"><Shield className="h-4 w-4 mr-1.5" />Security</TabsTrigger>
110
+ <TabsTrigger value="appearance"><Palette className="h-4 w-4 mr-1.5" />Appearance</TabsTrigger>
111
+ </TabsList>
112
+
113
+ <Card className="mt-4">
114
+ <TabsContent value="profile" className="m-0">
115
+ <CardHeader><CardTitle>Profile information</CardTitle><CardDescription>Update your name, phone and bio.</CardDescription></CardHeader>
116
+ <CardContent className="space-y-4">
117
+ <div className="grid gap-4 sm:grid-cols-2">
118
+ <div className="space-y-1.5"><Label>Full name</Label><Input value={name} onChange={(e) => setName(e.target.value)} /></div>
119
+ <div className="space-y-1.5"><Label>Phone</Label><Input defaultValue={user.phone} /></div>
120
+ <div className="space-y-1.5"><Label>Email</Label><Input value={user.email} disabled className="opacity-60" /></div>
121
+ <div className="space-y-1.5"><Label>Role</Label><Input value={user.role} disabled className="opacity-60" /><p className="text-xs text-muted-foreground">Assigned by your administrator.</p></div>
122
+ </div>
123
+ <div className="space-y-1.5"><Label>Bio</Label><Textarea rows={2} defaultValue="Fleet supervisor at Track Any Device." /></div>
124
+ <div className="flex justify-end"><Button size="sm">Save changes</Button></div>
125
+ </CardContent>
126
+ </TabsContent>
127
+
128
+ <TabsContent value="password" className="m-0">
129
+ <CardHeader><CardTitle>Change password</CardTitle></CardHeader>
130
+ <CardContent className="space-y-4">
131
+ <div className="space-y-1.5"><Label>Current password</Label><PasswordInput /></div>
132
+ <div className="grid gap-4 sm:grid-cols-2">
133
+ <div className="space-y-1.5"><Label>New password</Label><PasswordInput /></div>
134
+ <div className="space-y-1.5"><Label>Confirm</Label><PasswordInput /></div>
135
+ </div>
136
+ <div className="flex justify-end"><Button size="sm">Update password</Button></div>
137
+ </CardContent>
138
+ </TabsContent>
139
+
140
+ <TabsContent value="notifications" className="m-0">
141
+ <CardHeader><CardTitle>Notification preferences</CardTitle></CardHeader>
142
+ <CardContent>
143
+ {[
144
+ { label: 'Critical incidents', desc: 'SOS and high-priority violations', on: true },
145
+ { label: 'Device offline', desc: 'When a device stops reporting', on: true },
146
+ { label: 'Geofence violations', desc: 'Zone entry/exit alerts', on: true },
147
+ { label: 'Low battery', desc: 'Below 20% on any device', on: false },
148
+ { label: 'Weekly email digest', desc: 'Fleet summary every Monday', on: true },
149
+ ].map(({ label, desc, on }) => (
150
+ <div key={label} className="flex items-start justify-between py-2.5 border-b border-border last:border-0">
151
+ <div><p className="text-sm font-medium">{label}</p><p className="text-xs text-muted-foreground mt-0.5">{desc}</p></div>
152
+ <Switch defaultChecked={on} />
153
+ </div>
154
+ ))}
155
+ </CardContent>
156
+ </TabsContent>
157
+
158
+ <TabsContent value="security" className="m-0">
159
+ <CardHeader><CardTitle>Security</CardTitle></CardHeader>
160
+ <CardContent className="space-y-4">
161
+ <div className="flex items-center justify-between rounded-lg border border-border p-4">
162
+ <div><p className="text-sm font-medium">Two-factor authentication</p><p className="text-xs text-muted-foreground mt-0.5">Not enabled</p></div>
163
+ <Button size="sm">Enable 2FA</Button>
164
+ </div>
165
+ <div>
166
+ <p className="text-sm font-medium mb-3">Active sessions</p>
167
+ {[
168
+ { d: 'MacBook Pro — Chrome', t: 'Current session', current: true },
169
+ { d: 'iPhone 15 — Safari', t: '2 hours ago', current: false },
170
+ ].map((s) => (
171
+ <div key={s.d} className="flex items-center justify-between py-2 border-b border-border last:border-0">
172
+ <div><p className="text-sm">{s.d}</p><p className="text-xs text-muted-foreground">{s.t}</p></div>
173
+ {s.current
174
+ ? <Badge variant="outline" className="text-green-600 border-green-200 bg-green-50 text-xs">This device</Badge>
175
+ : <Button variant="ghost" size="sm" className="text-destructive h-7 px-2 text-xs">Revoke</Button>
176
+ }
177
+ </div>
178
+ ))}
179
+ </div>
180
+ </CardContent>
181
+ </TabsContent>
182
+
183
+ <TabsContent value="appearance" className="m-0">
184
+ <CardHeader><CardTitle>Appearance</CardTitle><CardDescription>Personalise how the app looks for you.</CardDescription></CardHeader>
185
+ <CardContent><AppearanceTabs /></CardContent>
186
+ </TabsContent>
187
+ </Card>
188
+ </Tabs>
189
+ </div>
190
+ </LayoutResolved>
191
+ );
192
+ }
193
+
194
+ export function UserSettingsSidebarPage({ layout, user, orgName }: {
195
+ layout: LayoutName;
196
+ user: TenantUser;
197
+ orgName: string;
198
+ }) {
199
+ const [active, setActive] = useState('info');
200
+ return (
201
+ <LayoutResolved layout={layout} title="Settings" currentUrl="/settings/profile">
202
+ <div className="flex min-h-full">
203
+ <aside className="hidden md:flex flex-col gap-0.5 w-52 shrink-0 sticky top-0 self-start max-h-screen overflow-y-auto border-r border-border p-3">
204
+ <p className="px-3 py-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-1">My Settings</p>
205
+ {SIDEBAR_NAV.map(({ id, label, icon: Icon }) => (
206
+ <button key={id} onClick={() => setActive(id)}
207
+ className={`flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-left transition-colors ${active === id ? 'bg-primary/10 text-primary font-medium' : 'hover:bg-muted text-foreground'}`}>
208
+ <Icon className="h-4 w-4 shrink-0" />{label}
209
+ </button>
210
+ ))}
211
+ </aside>
212
+
213
+ <div className="flex-1 p-6 space-y-6 max-w-xl">
214
+ <Card>
215
+ <CardHeader><CardTitle>Personal information</CardTitle></CardHeader>
216
+ <CardContent className="space-y-4">
217
+ <div className="flex items-center gap-3">
218
+ <Avatar className="h-12 w-12"><AvatarFallback>{user.initials}</AvatarFallback></Avatar>
219
+ <Button variant="outline" size="sm"><Camera className="h-3.5 w-3.5 mr-1.5" />Change photo</Button>
220
+ </div>
221
+ <div className="grid gap-3 sm:grid-cols-2">
222
+ <div className="space-y-1.5"><Label>Full name</Label><Input defaultValue={user.name} /></div>
223
+ <div className="space-y-1.5"><Label>Phone</Label><Input defaultValue={user.phone} /></div>
224
+ <div className="space-y-1.5"><Label>Country</Label><Select defaultValue="pk"><option value="pk">Pakistan</option></Select></div>
225
+ <div className="space-y-1.5"><Label>Timezone</Label><Select defaultValue="pkt"><option value="pkt">PKT (UTC+5)</option></Select></div>
226
+ </div>
227
+ <div className="space-y-1.5"><Label>Bio</Label><Textarea rows={2} defaultValue={`Fleet supervisor at ${orgName}.`} /></div>
228
+ <div className="flex justify-end"><Button size="sm">Save changes</Button></div>
229
+ </CardContent>
230
+ </Card>
231
+
232
+ <Card>
233
+ <CardHeader><CardTitle>Email &amp; password</CardTitle></CardHeader>
234
+ <CardContent className="space-y-4">
235
+ <div className="space-y-1.5"><Label>Email address</Label><Input type="email" defaultValue={user.email} /></div>
236
+ <Separator />
237
+ <div className="space-y-1.5"><Label>Current password</Label><PasswordInput /></div>
238
+ <div className="grid gap-3 sm:grid-cols-2">
239
+ <div className="space-y-1.5"><Label>New password</Label><PasswordInput /></div>
240
+ <div className="space-y-1.5"><Label>Confirm</Label><PasswordInput /></div>
241
+ </div>
242
+ <div className="flex justify-end"><Button size="sm">Update</Button></div>
243
+ </CardContent>
244
+ </Card>
245
+
246
+ <Card>
247
+ <CardHeader><CardTitle>Two-factor authentication</CardTitle></CardHeader>
248
+ <CardContent className="space-y-4">
249
+ <div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800 flex items-start gap-2">
250
+ <AlertTriangle className="h-4 w-4 mt-0.5 shrink-0 text-amber-600" />2FA is currently disabled. Enable it to secure your account.
251
+ </div>
252
+ <RadioGroup name="2fa" defaultValue="app" options={[
253
+ { value: 'app', label: 'Authenticator app', description: 'Google Authenticator or Authy' },
254
+ { value: 'sms', label: 'SMS code', description: user.phone },
255
+ ]} />
256
+ <Button size="sm">Enable 2FA</Button>
257
+ </CardContent>
258
+ </Card>
259
+
260
+ <Card>
261
+ <CardHeader><CardTitle>Social sign-in</CardTitle></CardHeader>
262
+ <CardContent>
263
+ {[{ name: 'Google', connected: true, account: 'ahmad@gmail.com' }, { name: 'Microsoft', connected: false }, { name: 'Apple', connected: false }].map(({ name, connected, account }) => (
264
+ <div key={name} className="flex items-center justify-between py-2.5 border-b border-border last:border-0">
265
+ <div><p className="text-sm font-medium">{name}</p>{account && <p className="text-xs text-muted-foreground">{account}</p>}</div>
266
+ {connected
267
+ ? <Button variant="outline" size="sm" className="text-destructive border-destructive/30">Disconnect</Button>
268
+ : <Button variant="outline" size="sm"><Plus className="h-3.5 w-3.5 mr-1" />Connect</Button>
269
+ }
270
+ </div>
271
+ ))}
272
+ </CardContent>
273
+ </Card>
274
+
275
+ <Card>
276
+ <CardHeader><CardTitle>Notifications</CardTitle></CardHeader>
277
+ <CardContent>
278
+ {[
279
+ { label: 'Critical incidents', on: true },
280
+ { label: 'Device offline', on: true },
281
+ { label: 'Geofence alerts', on: true },
282
+ { label: 'Low battery', on: false },
283
+ { label: 'Weekly digest email', on: true },
284
+ ].map(({ label, on }) => (
285
+ <div key={label} className="flex items-center justify-between py-2.5 border-b border-border last:border-0">
286
+ <p className="text-sm">{label}</p>
287
+ <Switch defaultChecked={on} />
288
+ </div>
289
+ ))}
290
+ </CardContent>
291
+ </Card>
292
+
293
+ <Card>
294
+ <CardHeader><CardTitle>Appearance</CardTitle></CardHeader>
295
+ <CardContent className="space-y-4">
296
+ <AppearanceTabs />
297
+ <Separator />
298
+ <div className="flex items-center justify-between">
299
+ <div><p className="text-sm font-medium">Compact sidebar</p><p className="text-xs text-muted-foreground">Icon-only sidebar</p></div>
300
+ <Switch />
301
+ </div>
302
+ </CardContent>
303
+ </Card>
304
+
305
+ <Card>
306
+ <CardHeader><CardTitle>Preferences</CardTitle></CardHeader>
307
+ <CardContent className="space-y-4">
308
+ <div className="grid gap-3 sm:grid-cols-2">
309
+ <div className="space-y-1.5"><Label>Language</Label><Select defaultValue="en"><option value="en">English</option><option value="ur">Urdu</option></Select></div>
310
+ <div className="space-y-1.5"><Label>Date format</Label><Select defaultValue="dmy"><option value="dmy">DD/MM/YYYY</option></Select></div>
311
+ </div>
312
+ <div className="space-y-2">
313
+ <p className="text-sm font-medium">Email visibility</p>
314
+ <RadioGroup name="vis" defaultValue="team" options={[
315
+ { value: 'public', label: 'Public' },
316
+ { value: 'team', label: 'Team members only' },
317
+ { value: 'private', label: 'Private' },
318
+ ]} />
319
+ </div>
320
+ <div className="flex justify-end"><Button size="sm">Save</Button></div>
321
+ </CardContent>
322
+ </Card>
323
+
324
+ <Card>
325
+ <CardHeader><CardTitle>API access</CardTitle></CardHeader>
326
+ <CardContent className="space-y-3">
327
+ <div className="space-y-1.5">
328
+ <Label>Personal access token</Label>
329
+ <div className="flex gap-2">
330
+ <Input value="tad_live_••••••••••••••••••••••••" readOnly className="font-mono text-xs" />
331
+ <Button variant="outline" size="icon"><Copy className="h-4 w-4" /></Button>
332
+ </div>
333
+ <p className="text-xs text-muted-foreground">Expires 15 Jan 2027</p>
334
+ </div>
335
+ </CardContent>
336
+ </Card>
337
+
338
+ <Card className="border-destructive/30">
339
+ <CardHeader><CardTitle className="text-destructive">Danger zone</CardTitle></CardHeader>
340
+ <CardContent className="space-y-4">
341
+ <div className="rounded-lg border border-destructive/40 bg-destructive/5 p-4 space-y-2">
342
+ <p className="text-sm font-semibold text-destructive flex gap-1.5 items-center"><AlertTriangle className="h-4 w-4" />Leave organisation</p>
343
+ <p className="text-xs text-muted-foreground">You will lose access to all fleet data in {orgName}. This cannot be undone.</p>
344
+ </div>
345
+ <div className="flex gap-2">
346
+ <Button variant="outline" size="sm" className="text-destructive border-destructive/40">Leave {orgName}</Button>
347
+ <Button variant="destructive" size="sm"><Trash2 className="h-3.5 w-3.5 mr-1.5" />Delete account</Button>
348
+ </div>
349
+ </CardContent>
350
+ </Card>
351
+ </div>
352
+ </div>
353
+ </LayoutResolved>
354
+ );
355
+ }
356
+
357
+ export function CompanyProfilePage({ layout, org, members }: {
358
+ layout: LayoutName;
359
+ org: TenantOrg;
360
+ members: OrgMember[];
361
+ }) {
362
+ return (
363
+ <LayoutResolved layout={layout} title="Organisation" currentUrl="/settings/organisation">
364
+ <div className="p-6 max-w-4xl space-y-6">
365
+ <h1 className="text-xl font-semibold">Organisation settings</h1>
366
+
367
+ <Tabs defaultValue="general">
368
+ <TabsList>
369
+ <TabsTrigger value="general"><Building2 className="h-4 w-4 mr-1.5" />General</TabsTrigger>
370
+ <TabsTrigger value="branding"><Palette className="h-4 w-4 mr-1.5" />Branding</TabsTrigger>
371
+ <TabsTrigger value="members"><Users className="h-4 w-4 mr-1.5" />Members</TabsTrigger>
372
+ <TabsTrigger value="account"><UserCog className="h-4 w-4 mr-1.5" />Account</TabsTrigger>
373
+ </TabsList>
374
+
375
+ <Card className="mt-4">
376
+ <TabsContent value="general" className="m-0">
377
+ <CardHeader><CardTitle>General information</CardTitle><CardDescription>Legal and contact details of your organisation.</CardDescription></CardHeader>
378
+ <CardContent>
379
+ <div className="grid gap-4 sm:grid-cols-2">
380
+ <div className="space-y-1.5"><Label>Organisation name</Label><Input defaultValue={org.name} /></div>
381
+ <div className="space-y-1.5"><Label>Slug / subdomain</Label><Input defaultValue={org.slug} /></div>
382
+ <div className="space-y-1.5"><Label>Contact email</Label><Input type="email" defaultValue={org.email} /></div>
383
+ <div className="space-y-1.5"><Label>Phone number</Label><Input defaultValue={org.phone} /></div>
384
+ <div className="space-y-1.5"><Label>Website</Label><Input type="url" defaultValue={org.website} /></div>
385
+ <div className="space-y-1.5"><Label>Country</Label><Select defaultValue="pk"><option value="pk">Pakistan</option></Select></div>
386
+ </div>
387
+ <Separator className="my-4" />
388
+ <p className="text-sm font-medium mb-3">Registration details</p>
389
+ <div className="grid gap-4 sm:grid-cols-2">
390
+ <div className="space-y-1.5"><Label>VAT / NTN number</Label>
391
+ <div className="flex gap-2"><Input defaultValue={org.vat} /><Button variant="outline" size="icon"><Copy className="h-4 w-4" /></Button></div>
392
+ </div>
393
+ <div className="space-y-1.5"><Label>Registration number</Label><Input defaultValue={org.reg} /></div>
394
+ </div>
395
+ <div className="flex justify-end mt-4"><Button size="sm">Save changes</Button></div>
396
+ </CardContent>
397
+ </TabsContent>
398
+
399
+ <TabsContent value="branding" className="m-0">
400
+ <CardHeader><CardTitle>Branding</CardTitle><CardDescription>Logo, brand colour and white-label settings.</CardDescription></CardHeader>
401
+ <CardContent className="space-y-6">
402
+ <div>
403
+ <p className="text-sm font-medium mb-2">Organisation logo</p>
404
+ <div className="flex items-center gap-4">
405
+ <div className="h-16 w-16 rounded-xl border-2 border-dashed border-border flex items-center justify-center bg-muted/40">
406
+ <Building2 className="h-7 w-7 text-muted-foreground" />
407
+ </div>
408
+ <div className="space-y-1.5">
409
+ <Button variant="outline" size="sm"><Camera className="h-3.5 w-3.5 mr-1.5" />Upload logo</Button>
410
+ <p className="text-xs text-muted-foreground">PNG or SVG, min 200×200px, max 1 MB</p>
411
+ </div>
412
+ </div>
413
+ </div>
414
+ <Separator />
415
+ <div className="space-y-1.5">
416
+ <Label>Brand colour</Label>
417
+ <div className="flex items-center gap-3">
418
+ <input type="color" defaultValue="#2563eb" className="h-10 w-14 cursor-pointer rounded-lg border border-border p-1" />
419
+ <Input defaultValue="#2563EB" className="w-32 font-mono text-sm" />
420
+ </div>
421
+ </div>
422
+ <Separator />
423
+ <div className="space-y-3">
424
+ <p className="text-sm font-medium">White-label options</p>
425
+ <Checkbox id="wl-emails" label="Apply branding to system emails" defaultChecked />
426
+ <Checkbox id="wl-reports" label="Apply branding to PDF reports" defaultChecked />
427
+ <Checkbox id="wl-portal" label="Use custom login portal URL" />
428
+ </div>
429
+ <div className="flex justify-end"><Button size="sm">Save branding</Button></div>
430
+ </CardContent>
431
+ </TabsContent>
432
+
433
+ <TabsContent value="members" className="m-0">
434
+ <CardHeader className="flex flex-row items-center justify-between">
435
+ <div><CardTitle>Team members</CardTitle><CardDescription>{members.length} members · 2FA required for admins</CardDescription></div>
436
+ <div className="flex items-center gap-2">
437
+ <div className="flex items-center gap-2 text-sm">
438
+ <span className="text-muted-foreground">Enforce 2FA</span>
439
+ <Switch defaultChecked />
440
+ </div>
441
+ <Button size="sm"><Plus className="h-3.5 w-3.5 mr-1" />Invite</Button>
442
+ </div>
443
+ </CardHeader>
444
+ <CardContent className="p-0">
445
+ <Table>
446
+ <TableHeader>
447
+ <TableRow>
448
+ <TableHead>Member</TableHead>
449
+ <TableHead>Role</TableHead>
450
+ <TableHead>2FA</TableHead>
451
+ <TableHead>Joined</TableHead>
452
+ <TableHead />
453
+ </TableRow>
454
+ </TableHeader>
455
+ <TableBody>
456
+ {members.map((m) => (
457
+ <TableRow key={m.email}>
458
+ <TableCell>
459
+ <div className="flex items-center gap-2.5">
460
+ <Avatar className="h-7 w-7"><AvatarFallback className="text-xs">{m.initials}</AvatarFallback></Avatar>
461
+ <div>
462
+ <p className="text-sm font-medium">{m.name}</p>
463
+ <p className="text-xs text-muted-foreground">{m.email}</p>
464
+ </div>
465
+ </div>
466
+ </TableCell>
467
+ <TableCell><Badge variant="outline" className="text-xs">{m.role}</Badge></TableCell>
468
+ <TableCell>
469
+ {m.twofa
470
+ ? <Badge variant="outline" className="text-green-600 border-green-200 bg-green-50 text-xs">Enabled</Badge>
471
+ : <Badge variant="outline" className="text-amber-600 border-amber-200 bg-amber-50 text-xs">Disabled</Badge>
472
+ }
473
+ </TableCell>
474
+ <TableCell className="text-sm text-muted-foreground">{m.joined}</TableCell>
475
+ <TableCell className="text-right">
476
+ <Button variant="ghost" size="sm" className="h-7 px-2"><ChevronRight className="h-4 w-4" /></Button>
477
+ </TableCell>
478
+ </TableRow>
479
+ ))}
480
+ </TableBody>
481
+ </Table>
482
+ </CardContent>
483
+ </TabsContent>
484
+
485
+ <TabsContent value="account" className="m-0">
486
+ <CardHeader><CardTitle>Account settings</CardTitle><CardDescription>Login, social accounts and referral settings.</CardDescription></CardHeader>
487
+ <CardContent>
488
+ <table className="w-full text-sm">
489
+ <tbody>
490
+ {[
491
+ { label: 'Login email', value: org.email, action: 'Change' },
492
+ { label: 'Password', value: '••••••••••', action: 'Change' },
493
+ { label: 'Sign-in options',value: 'Google, Microsoft', action: 'Manage' },
494
+ { label: 'Team account', value: 'Enabled', action: 'Configure' },
495
+ { label: 'Referral link', value: `https://tad.io/ref/${org.slug}`, action: 'Copy' },
496
+ ].map(({ label, value, action }) => (
497
+ <tr key={label} className="border-b border-border last:border-0">
498
+ <td className="py-3 pr-4 text-muted-foreground w-36">{label}</td>
499
+ <td className="py-3 pr-4 font-medium truncate max-w-xs">{value}</td>
500
+ <td className="py-3 text-right"><Button variant="ghost" size="sm" className="text-primary h-6 px-1.5 text-xs">{action}</Button></td>
501
+ </tr>
502
+ ))}
503
+ </tbody>
504
+ </table>
505
+ </CardContent>
506
+ </TabsContent>
507
+ </Card>
508
+ </Tabs>
509
+ </div>
510
+ </LayoutResolved>
511
+ );
512
+ }
513
+
514
+ export function SecuritySettingsPage({ layout, securityLog }: { layout: LayoutName; securityLog: SecurityLogEntry[] }) {
515
+ return (
516
+ <LayoutResolved layout={layout} title="Security" currentUrl="/settings/security">
517
+ <div className="p-6 max-w-3xl space-y-6">
518
+ <h1 className="text-xl font-semibold">Security settings</h1>
519
+
520
+ <Card>
521
+ <CardHeader><CardTitle>Two-factor authentication</CardTitle><CardDescription>Manage 2FA requirements for your organisation.</CardDescription></CardHeader>
522
+ <CardContent className="space-y-4">
523
+ <div className="flex items-center justify-between rounded-lg border border-border p-4">
524
+ <div>
525
+ <p className="text-sm font-semibold">Enforce 2FA for all members</p>
526
+ <p className="text-xs text-muted-foreground mt-0.5">Members without 2FA will be prompted to enable it on next login.</p>
527
+ </div>
528
+ <Switch defaultChecked />
529
+ </div>
530
+ <div className="flex items-center justify-between rounded-lg border border-border p-4">
531
+ <div>
532
+ <p className="text-sm font-semibold">Require 2FA for admin roles</p>
533
+ <p className="text-xs text-muted-foreground mt-0.5">Admins and supervisors must use 2FA.</p>
534
+ </div>
535
+ <Switch defaultChecked />
536
+ </div>
537
+ <div className="grid grid-cols-3 gap-4 pt-1">
538
+ {[
539
+ { label: 'Members with 2FA', value: '3 / 5', color: 'text-green-600' },
540
+ { label: 'Admin 2FA coverage', value: '2 / 2', color: 'text-green-600' },
541
+ { label: 'At risk', value: '2 members', color: 'text-amber-600' },
542
+ ].map(({ label, value, color }) => (
543
+ <div key={label} className="rounded-lg border border-border p-3 text-center">
544
+ <p className={`text-base font-bold ${color}`}>{value}</p>
545
+ <p className="text-xs text-muted-foreground mt-0.5">{label}</p>
546
+ </div>
547
+ ))}
548
+ </div>
549
+ </CardContent>
550
+ </Card>
551
+
552
+ <Card>
553
+ <CardHeader><CardTitle>Session policy</CardTitle></CardHeader>
554
+ <CardContent className="space-y-4">
555
+ <div className="grid gap-4 sm:grid-cols-2">
556
+ <div className="space-y-1.5">
557
+ <Label>Session timeout</Label>
558
+ <Select defaultValue="8h">
559
+ <option value="1h">1 hour</option>
560
+ <option value="8h">8 hours</option>
561
+ <option value="24h">24 hours</option>
562
+ <option value="7d">7 days</option>
563
+ </Select>
564
+ </div>
565
+ <div className="space-y-1.5">
566
+ <Label>Max concurrent sessions</Label>
567
+ <Select defaultValue="3"><option value="1">1</option><option value="3">3</option><option value="10">Unlimited</option></Select>
568
+ </div>
569
+ </div>
570
+ <div className="flex items-center justify-between">
571
+ <div><p className="text-sm font-medium">Restrict login to Pakistan only</p><p className="text-xs text-muted-foreground">Block sign-in attempts from foreign IPs</p></div>
572
+ <Switch defaultChecked />
573
+ </div>
574
+ <div className="flex justify-end"><Button size="sm">Save policy</Button></div>
575
+ </CardContent>
576
+ </Card>
577
+
578
+ <Card>
579
+ <CardHeader className="flex flex-row items-center justify-between">
580
+ <div><CardTitle>IP whitelist</CardTitle><CardDescription>Only these IPs can access the admin panel.</CardDescription></div>
581
+ <Button variant="outline" size="sm"><Plus className="h-3.5 w-3.5 mr-1" />Add IP</Button>
582
+ </CardHeader>
583
+ <CardContent>
584
+ {['203.128.1.44 — Lahore office', '182.180.64.0/24 — Punjab data centre'].map((ip) => (
585
+ <div key={ip} className="flex items-center justify-between py-2.5 border-b border-border last:border-0">
586
+ <p className="text-sm font-mono">{ip}</p>
587
+ <Button variant="ghost" size="sm" className="text-destructive h-7 px-2"><Trash2 className="h-3.5 w-3.5" /></Button>
588
+ </div>
589
+ ))}
590
+ </CardContent>
591
+ </Card>
592
+
593
+ <Card>
594
+ <CardHeader><CardTitle>Security log</CardTitle><CardDescription>Recent security-relevant events in your organisation.</CardDescription></CardHeader>
595
+ <CardContent className="p-0">
596
+ <Table>
597
+ <TableHeader><TableRow><TableHead>Event</TableHead><TableHead>User</TableHead><TableHead>IP</TableHead><TableHead>Date</TableHead><TableHead>Status</TableHead></TableRow></TableHeader>
598
+ <TableBody>
599
+ {securityLog.map((row, i) => (
600
+ <TableRow key={i}>
601
+ <TableCell className="text-sm font-medium">{row.event}</TableCell>
602
+ <TableCell className="text-sm text-muted-foreground">{row.user}</TableCell>
603
+ <TableCell className="text-xs font-mono text-muted-foreground">{row.ip}</TableCell>
604
+ <TableCell className="text-xs text-muted-foreground">{row.date}</TableCell>
605
+ <TableCell>
606
+ <Badge variant="outline" className={`text-xs ${row.ok ? 'text-green-600 border-green-200 bg-green-50' : 'text-red-600 border-red-200 bg-red-50'}`}>
607
+ {row.ok ? 'Success' : 'Warning'}
608
+ </Badge>
609
+ </TableCell>
610
+ </TableRow>
611
+ ))}
612
+ </TableBody>
613
+ </Table>
614
+ </CardContent>
615
+ </Card>
616
+ </div>
617
+ </LayoutResolved>
618
+ );
619
+ }
620
+
621
+ export function TenantBillingPage({ layout, org, invoices }: {
622
+ layout: LayoutName;
623
+ org: TenantOrg;
624
+ invoices: TenantInvoice[];
625
+ }) {
626
+ return (
627
+ <LayoutResolved layout={layout} title="Billing" currentUrl="/settings/billing">
628
+ <div className="p-6 max-w-3xl space-y-6">
629
+ <h1 className="text-xl font-semibold">Billing &amp; plan</h1>
630
+
631
+ <PlanCard
632
+ name="Enterprise Plan"
633
+ status="Active"
634
+ price="PKR 24,500 / month · Up to 1,000 devices · Unlimited users"
635
+ renewal={`Renews 1 June 2026 · Billed to ${org.name} · NTN ${org.vat}`}
636
+ action={<Button variant="outline" size="sm"><Star className="h-3.5 w-3.5 mr-1.5 text-amber-500" />Upgrade</Button>}
637
+ columns={4}
638
+ usage={[
639
+ { label: 'Devices', used: 312, limit: 1000 },
640
+ { label: 'Users', used: 5, limit: 999 },
641
+ { label: 'Storage', used: 28, limit: 100 },
642
+ { label: 'API calls', used: 142, limit: 500 },
643
+ ]}
644
+ />
645
+
646
+ <Card>
647
+ <CardHeader className="flex flex-row items-center justify-between">
648
+ <CardTitle>Payment method</CardTitle>
649
+ <Button variant="outline" size="sm"><Plus className="h-3.5 w-3.5 mr-1" />Add</Button>
650
+ </CardHeader>
651
+ <CardContent className="space-y-3">
652
+ {[
653
+ { label: 'Meezan Bank — •••• 4242', exp: '09/2028', primary: true },
654
+ { label: 'HBL — •••• 8881', exp: '03/2027', primary: false },
655
+ ].map(({ label, exp, primary }) => (
656
+ <div key={label} className="flex items-center justify-between rounded-lg border border-border p-3.5">
657
+ <div className="flex items-center gap-3">
658
+ <div className="rounded-lg bg-muted p-2"><CreditCard className="h-4 w-4 text-muted-foreground" /></div>
659
+ <div><p className="text-sm font-medium">{label}</p><p className="text-xs text-muted-foreground">Expires {exp}</p></div>
660
+ </div>
661
+ <div className="flex items-center gap-2">
662
+ {primary && <Badge variant="outline" className="text-green-600 border-green-200 bg-green-50 text-xs">Default</Badge>}
663
+ {!primary && <Button variant="ghost" size="sm" className="h-7 px-2 text-xs">Set default</Button>}
664
+ <Button variant="ghost" size="sm" className="h-7 px-2 text-destructive"><Trash2 className="h-3.5 w-3.5" /></Button>
665
+ </div>
666
+ </div>
667
+ ))}
668
+ </CardContent>
669
+ </Card>
670
+
671
+ <Card>
672
+ <CardHeader><CardTitle>Invoice history</CardTitle></CardHeader>
673
+ <CardContent className="p-0">
674
+ <Table>
675
+ <TableHeader><TableRow><TableHead>Invoice</TableHead><TableHead>Date</TableHead><TableHead>Amount</TableHead><TableHead>Status</TableHead><TableHead /></TableRow></TableHeader>
676
+ <TableBody>
677
+ {invoices.map((inv) => (
678
+ <TableRow key={inv.id}>
679
+ <TableCell className="font-mono text-xs font-medium">{inv.id}</TableCell>
680
+ <TableCell className="text-sm text-muted-foreground">{inv.date}</TableCell>
681
+ <TableCell className="text-sm font-medium">{inv.amount}</TableCell>
682
+ <TableCell><Badge variant="outline" className="text-green-600 border-green-200 bg-green-50 text-xs">{inv.status}</Badge></TableCell>
683
+ <TableCell className="text-right"><Button variant="ghost" size="sm" className="h-7 px-2"><Download className="h-3.5 w-3.5" /></Button></TableCell>
684
+ </TableRow>
685
+ ))}
686
+ </TableBody>
687
+ </Table>
688
+ </CardContent>
689
+ </Card>
690
+
691
+ <Card className="border-destructive/30">
692
+ <CardHeader><CardTitle className="text-destructive">Cancel subscription</CardTitle></CardHeader>
693
+ <CardContent>
694
+ <p className="text-sm text-muted-foreground mb-4">Cancelling will disable device tracking at the end of the billing period. Historical data is retained for 60 days.</p>
695
+ <Button variant="outline" size="sm" className="text-destructive border-destructive/40">Cancel Enterprise plan</Button>
696
+ </CardContent>
697
+ </Card>
698
+ </div>
699
+ </LayoutResolved>
700
+ );
701
+ }