@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.
- package/README.md +9 -9
- package/package.json +133 -4
- package/src/assets/index.ts +120 -0
- package/src/assets/media/avatars/300-1.png +0 -0
- package/src/assets/media/avatars/300-10.png +0 -0
- package/src/assets/media/avatars/300-11.png +0 -0
- package/src/assets/media/avatars/300-12.png +0 -0
- package/src/assets/media/avatars/300-13.png +0 -0
- package/src/assets/media/avatars/300-14.png +0 -0
- package/src/assets/media/avatars/300-15.png +0 -0
- package/src/assets/media/avatars/300-16.png +0 -0
- package/src/assets/media/avatars/300-17.png +0 -0
- package/src/assets/media/avatars/300-18.png +0 -0
- package/src/assets/media/avatars/300-19.png +0 -0
- package/src/assets/media/avatars/300-2.png +0 -0
- package/src/assets/media/avatars/300-20.png +0 -0
- package/src/assets/media/avatars/300-21.png +0 -0
- package/src/assets/media/avatars/300-22.png +0 -0
- package/src/assets/media/avatars/300-23.png +0 -0
- package/src/assets/media/avatars/300-24.png +0 -0
- package/src/assets/media/avatars/300-25.png +0 -0
- package/src/assets/media/avatars/300-26.png +0 -0
- package/src/assets/media/avatars/300-27.png +0 -0
- package/src/assets/media/avatars/300-28.png +0 -0
- package/src/assets/media/avatars/300-29.png +0 -0
- package/src/assets/media/avatars/300-3.png +0 -0
- package/src/assets/media/avatars/300-30.png +0 -0
- package/src/assets/media/avatars/300-31.png +0 -0
- package/src/assets/media/avatars/300-32.png +0 -0
- package/src/assets/media/avatars/300-33.png +0 -0
- package/src/assets/media/avatars/300-34.png +0 -0
- package/src/assets/media/avatars/300-4.png +0 -0
- package/src/assets/media/avatars/300-5.png +0 -0
- package/src/assets/media/avatars/300-6.png +0 -0
- package/src/assets/media/avatars/300-7.png +0 -0
- package/src/assets/media/avatars/300-8.png +0 -0
- package/src/assets/media/avatars/300-9.png +0 -0
- package/src/assets/media/avatars/blank.png +0 -0
- package/src/assets/media/avatars/gray/1.png +0 -0
- package/src/assets/media/avatars/gray/2.png +0 -0
- package/src/assets/media/avatars/gray/3.png +0 -0
- package/src/assets/media/avatars/gray/4.png +0 -0
- package/src/assets/media/avatars/gray/5.png +0 -0
- package/src/assets/media/illustrations/1-dark.svg +78 -0
- package/src/assets/media/illustrations/1.svg +78 -0
- package/src/assets/media/illustrations/10-dark.svg +148 -0
- package/src/assets/media/illustrations/10.svg +148 -0
- package/src/assets/media/illustrations/11-dark.svg +234 -0
- package/src/assets/media/illustrations/11.svg +234 -0
- package/src/assets/media/illustrations/12.svg +138 -0
- package/src/assets/media/illustrations/13.svg +205 -0
- package/src/assets/media/illustrations/14.svg +259 -0
- package/src/assets/media/illustrations/15.svg +242 -0
- package/src/assets/media/illustrations/16.svg +128 -0
- package/src/assets/media/illustrations/17.svg +180 -0
- package/src/assets/media/illustrations/18-dark.svg +6 -0
- package/src/assets/media/illustrations/18.svg +6 -0
- package/src/assets/media/illustrations/19-dark.svg +8 -0
- package/src/assets/media/illustrations/19.svg +8 -0
- package/src/assets/media/illustrations/2-dark.svg +78 -0
- package/src/assets/media/illustrations/2.svg +78 -0
- package/src/assets/media/illustrations/20-dark.svg +13 -0
- package/src/assets/media/illustrations/20.svg +13 -0
- package/src/assets/media/illustrations/21-dark.svg +9 -0
- package/src/assets/media/illustrations/21.svg +9 -0
- package/src/assets/media/illustrations/22-dark.svg +17 -0
- package/src/assets/media/illustrations/22.svg +17 -0
- package/src/assets/media/illustrations/23-dark.svg +13 -0
- package/src/assets/media/illustrations/23.svg +13 -0
- package/src/assets/media/illustrations/24.svg +6 -0
- package/src/assets/media/illustrations/25.svg +8 -0
- package/src/assets/media/illustrations/26.svg +8 -0
- package/src/assets/media/illustrations/27.svg +6 -0
- package/src/assets/media/illustrations/28-dark.svg +28 -0
- package/src/assets/media/illustrations/28.svg +14 -0
- package/src/assets/media/illustrations/29-dark.svg +6 -0
- package/src/assets/media/illustrations/29.svg +6 -0
- package/src/assets/media/illustrations/3-dark.svg +70 -0
- package/src/assets/media/illustrations/3.svg +70 -0
- package/src/assets/media/illustrations/30-dark.svg +8 -0
- package/src/assets/media/illustrations/30.svg +8 -0
- package/src/assets/media/illustrations/31-dark.svg +9 -0
- package/src/assets/media/illustrations/31.svg +9 -0
- package/src/assets/media/illustrations/32-dark.svg +10 -0
- package/src/assets/media/illustrations/32.svg +10 -0
- package/src/assets/media/illustrations/33-dark.svg +15 -0
- package/src/assets/media/illustrations/33.svg +15 -0
- package/src/assets/media/illustrations/34-dark.svg +5 -0
- package/src/assets/media/illustrations/34.svg +5 -0
- package/src/assets/media/illustrations/35-dark.svg +11 -0
- package/src/assets/media/illustrations/35.svg +4 -0
- package/src/assets/media/illustrations/4-dark.svg +51 -0
- package/src/assets/media/illustrations/4.svg +51 -0
- package/src/assets/media/illustrations/5-dark.svg +78 -0
- package/src/assets/media/illustrations/5.svg +78 -0
- package/src/assets/media/illustrations/6.svg +58 -0
- package/src/assets/media/illustrations/7.svg +49 -0
- package/src/assets/media/illustrations/8.svg +61 -0
- package/src/assets/media/illustrations/9.svg +57 -0
- package/src/assets/media/misc/placeholder.svg +15 -0
- package/src/components/devices/devices-mini-map.tsx +32 -26
- package/src/components/devices/map-marker.tsx +98 -0
- package/src/components/ui/checklist-item.tsx +55 -0
- package/src/components/ui/plan-card.tsx +68 -0
- package/src/components/ui/settings-row.tsx +32 -0
- package/src/components/ui/settings-section.tsx +22 -0
- package/src/components/ui/usage-meter.tsx +35 -0
- package/src/index.ts +12 -1
- package/src/layouts/LayoutSwitcher.tsx +220 -0
- package/src/layouts/app/MegaMenuLayout.tsx +69 -34
- package/src/layouts/app/MegaMenuNavbarLayout.tsx +73 -37
- package/src/layouts/app/NavbarCollapsibleLayout.tsx +53 -4
- package/src/layouts/app/NavbarSidebarLayout.tsx +74 -29
- package/src/layouts/app/SidebarDualMenuLayout.tsx +48 -5
- package/src/layouts/app/SidebarFixedLayout.tsx +15 -10
- package/src/layouts/app/SidebarMinimalLayout.tsx +51 -3
- package/src/layouts/app/SidebarTabsLayout.tsx +48 -2
- package/src/layouts/app/SplitSidebarLayout.tsx +91 -43
- package/src/layouts/app/TopNavLayout.tsx +7 -12
- package/src/layouts/app/WorkspaceSidebarLayout.tsx +103 -46
- package/src/layouts/app/partials/Navbar.tsx +61 -10
- package/src/layouts/app/partials/Toolbar.tsx +1 -1
- package/src/layouts/auth/AuthCenteredLayout.tsx +10 -4
- package/src/lib/map-markers.ts +21 -3
- package/src/pages/login/ConfirmPasswordPage.tsx +35 -0
- package/src/pages/login/ForgotPasswordPage.tsx +41 -0
- package/src/pages/login/LoginPage.tsx +50 -0
- package/src/pages/login/RegisterPage.tsx +41 -0
- package/src/pages/login/ResetPasswordPage.tsx +35 -0
- package/src/pages/login/TwoFactorChallengePage.tsx +41 -0
- package/src/pages/login/VerifyEmailPage.tsx +31 -0
- package/src/pages/my/ActivityPage.tsx +160 -0
- package/src/pages/my/GetStartedPage.tsx +221 -0
- package/src/pages/my/NotificationsPage.tsx +133 -0
- package/src/pages/my/ProfilePage.tsx +650 -0
- package/src/pages/my/TenantsPage.tsx +37 -0
- package/src/pages/tenant/AssigneesPage.tsx +155 -0
- package/src/pages/tenant/BeatsPage.tsx +403 -0
- package/src/pages/tenant/DashboardPage.tsx +195 -0
- package/src/pages/tenant/GeofencePage.tsx +422 -0
- package/src/pages/tenant/IncidentsPage.tsx +214 -0
- package/src/pages/tenant/IntegrationsPage.tsx +352 -0
- package/src/pages/tenant/InvitePage.tsx +153 -0
- package/src/pages/tenant/LiveStreamPage.tsx +141 -0
- package/src/pages/tenant/MembersPage.tsx +414 -0
- package/src/pages/tenant/TenantProfilePage.tsx +701 -0
- package/src/platform/adapters/default.tsx +1 -1
- package/src/platform/types.ts +2 -0
- package/src/styles/components/apexcharts.css +101 -0
- package/src/styles/components/image-input.css +51 -0
- package/src/styles/components/leaflet.css +25 -0
- package/src/styles/components/rating.css +89 -0
- package/src/styles/components/scrollable.css +119 -0
- package/src/styles/layout.css +24 -0
- package/src/styles/layouts/sidebar-fixed.css +93 -138
- package/src/styles/themes.css +5 -5
- package/src/vite-env.d.ts +21 -0
- package/src/layouts/SettingsLayout.tsx +0 -21
- package/src/layouts/app-layout.tsx +0 -29
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Avatar, AvatarFallback,
|
|
3
|
+
Badge, Button,
|
|
4
|
+
Card, CardContent, CardFooter, CardHeader, CardTitle, CardDescription,
|
|
5
|
+
Checkbox,
|
|
6
|
+
Input, Label,
|
|
7
|
+
Select,
|
|
8
|
+
Switch,
|
|
9
|
+
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
|
10
|
+
} from '@trackany-device/components';
|
|
11
|
+
import { LayoutResolved } from '../../layouts/LayoutSwitcher';
|
|
12
|
+
import type { LayoutName } from '../../layouts/LayoutSwitcher';
|
|
13
|
+
import {
|
|
14
|
+
Check, Copy, Download, Filter, Link2, Mail, MoreHorizontal,
|
|
15
|
+
Plus, Search, Settings, Shield, ShieldCheck, UserPlus, Users, X,
|
|
16
|
+
} from 'lucide-react';
|
|
17
|
+
import { useState } from 'react';
|
|
18
|
+
|
|
19
|
+
export type MemberRole = 'Admin' | 'Supervisor' | 'Operator' | 'Member';
|
|
20
|
+
export type MemberStatus = 'Active' | 'Invited' | 'Inactive';
|
|
21
|
+
|
|
22
|
+
export interface Member {
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
role: MemberRole;
|
|
27
|
+
status: MemberStatus;
|
|
28
|
+
twofa: boolean;
|
|
29
|
+
initials: string;
|
|
30
|
+
joined: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface Role {
|
|
34
|
+
name: MemberRole;
|
|
35
|
+
color: string;
|
|
36
|
+
count: number;
|
|
37
|
+
desc: string;
|
|
38
|
+
perms: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface PermissionRow {
|
|
42
|
+
label: string;
|
|
43
|
+
admin: boolean;
|
|
44
|
+
supervisor: boolean;
|
|
45
|
+
operator: boolean;
|
|
46
|
+
member: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface PermissionSection {
|
|
50
|
+
section: string;
|
|
51
|
+
rows: PermissionRow[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const STATUS_COLOR: Record<MemberStatus, string> = {
|
|
55
|
+
Active: 'text-green-600 border-green-200 bg-green-50',
|
|
56
|
+
Invited: 'text-amber-600 border-amber-200 bg-amber-50',
|
|
57
|
+
Inactive: 'text-muted-foreground border-border bg-muted',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const ROLE_COLOR: Record<MemberRole, string> = {
|
|
61
|
+
Admin: 'text-purple-600 border-purple-200 bg-purple-50',
|
|
62
|
+
Supervisor: 'text-blue-600 border-blue-200 bg-blue-50',
|
|
63
|
+
Operator: 'text-amber-600 border-amber-200 bg-amber-50',
|
|
64
|
+
Member: 'text-green-600 border-green-200 bg-green-50',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function TeamMembersPage({ layout, members }: { layout: LayoutName; members: Member[] }) {
|
|
68
|
+
const [search, setSearch] = useState('');
|
|
69
|
+
const filtered = members.filter(
|
|
70
|
+
(m) => m.name.toLowerCase().includes(search.toLowerCase()) || m.email.includes(search.toLowerCase()),
|
|
71
|
+
);
|
|
72
|
+
return (
|
|
73
|
+
<LayoutResolved layout={layout}>
|
|
74
|
+
<div className="p-6 max-w-5xl mx-auto space-y-6">
|
|
75
|
+
<div className="flex items-center justify-between">
|
|
76
|
+
<div>
|
|
77
|
+
<h1 className="text-xl font-semibold">Team members</h1>
|
|
78
|
+
<p className="text-sm text-muted-foreground mt-0.5">{members.length} members · 5 of 10 seats used</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div className="flex gap-2">
|
|
81
|
+
<Button variant="outline" size="sm"><Download className="h-3.5 w-3.5 mr-1.5" />Export</Button>
|
|
82
|
+
<Button size="sm"><UserPlus className="h-4 w-4 mr-1.5" />Invite member</Button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div className="flex items-center gap-3">
|
|
87
|
+
<div className="relative flex-1 max-w-xs">
|
|
88
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
|
|
89
|
+
<Input placeholder="Search members…" className="pl-8 text-sm" value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
90
|
+
</div>
|
|
91
|
+
<Select defaultValue="all">
|
|
92
|
+
<option value="all">All roles</option>
|
|
93
|
+
<option value="admin">Admin</option>
|
|
94
|
+
<option value="supervisor">Supervisor</option>
|
|
95
|
+
<option value="operator">Operator</option>
|
|
96
|
+
<option value="member">Member</option>
|
|
97
|
+
</Select>
|
|
98
|
+
<Select defaultValue="all">
|
|
99
|
+
<option value="all">All statuses</option>
|
|
100
|
+
<option value="active">Active</option>
|
|
101
|
+
<option value="invited">Invited</option>
|
|
102
|
+
<option value="inactive">Inactive</option>
|
|
103
|
+
</Select>
|
|
104
|
+
<Button variant="outline" size="sm"><Filter className="h-3.5 w-3.5 mr-1.5" />More filters</Button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<Card>
|
|
108
|
+
<CardContent className="p-0">
|
|
109
|
+
<Table>
|
|
110
|
+
<TableHeader>
|
|
111
|
+
<TableRow>
|
|
112
|
+
<TableHead className="w-10"><Checkbox /></TableHead>
|
|
113
|
+
<TableHead>Member</TableHead>
|
|
114
|
+
<TableHead>Role</TableHead>
|
|
115
|
+
<TableHead>Status</TableHead>
|
|
116
|
+
<TableHead>2FA</TableHead>
|
|
117
|
+
<TableHead>Joined</TableHead>
|
|
118
|
+
<TableHead />
|
|
119
|
+
</TableRow>
|
|
120
|
+
</TableHeader>
|
|
121
|
+
<TableBody>
|
|
122
|
+
{filtered.map((m) => (
|
|
123
|
+
<TableRow key={m.id}>
|
|
124
|
+
<TableCell><Checkbox /></TableCell>
|
|
125
|
+
<TableCell>
|
|
126
|
+
<div className="flex items-center gap-2.5">
|
|
127
|
+
<Avatar className="h-8 w-8 shrink-0">
|
|
128
|
+
<AvatarFallback className="text-xs">{m.initials}</AvatarFallback>
|
|
129
|
+
</Avatar>
|
|
130
|
+
<div>
|
|
131
|
+
<p className="text-sm font-medium">{m.name}</p>
|
|
132
|
+
<p className="text-xs text-muted-foreground">{m.email}</p>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</TableCell>
|
|
136
|
+
<TableCell>
|
|
137
|
+
<Badge variant="outline" className={`text-xs ${ROLE_COLOR[m.role]}`}>{m.role}</Badge>
|
|
138
|
+
</TableCell>
|
|
139
|
+
<TableCell>
|
|
140
|
+
<Badge variant="outline" className={`text-xs ${STATUS_COLOR[m.status]}`}>{m.status}</Badge>
|
|
141
|
+
</TableCell>
|
|
142
|
+
<TableCell>
|
|
143
|
+
{m.twofa
|
|
144
|
+
? <ShieldCheck className="h-4 w-4 text-green-600" />
|
|
145
|
+
: <Shield className="h-4 w-4 text-muted-foreground/40" />}
|
|
146
|
+
</TableCell>
|
|
147
|
+
<TableCell className="text-sm text-muted-foreground">{m.joined}</TableCell>
|
|
148
|
+
<TableCell className="text-right">
|
|
149
|
+
<Button variant="ghost" size="icon" className="h-7 w-7">
|
|
150
|
+
<MoreHorizontal className="h-4 w-4" />
|
|
151
|
+
</Button>
|
|
152
|
+
</TableCell>
|
|
153
|
+
</TableRow>
|
|
154
|
+
))}
|
|
155
|
+
</TableBody>
|
|
156
|
+
</Table>
|
|
157
|
+
</CardContent>
|
|
158
|
+
</Card>
|
|
159
|
+
</div>
|
|
160
|
+
</LayoutResolved>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function InviteMemberPage({ layout }: { layout: LayoutName }) {
|
|
165
|
+
const [emails, setEmails] = useState(['']);
|
|
166
|
+
const [sent, setSent] = useState(false);
|
|
167
|
+
return (
|
|
168
|
+
<LayoutResolved layout={layout}>
|
|
169
|
+
<div className="p-6 max-w-xl mx-auto space-y-6">
|
|
170
|
+
<h1 className="text-xl font-semibold">Invite team members</h1>
|
|
171
|
+
|
|
172
|
+
{sent && (
|
|
173
|
+
<div className="flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-4 py-2.5 text-sm text-green-700">
|
|
174
|
+
<Check className="h-4 w-4 shrink-0" />Invitations sent successfully
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
|
|
178
|
+
<Card>
|
|
179
|
+
<CardHeader>
|
|
180
|
+
<CardTitle>Send invitations</CardTitle>
|
|
181
|
+
<CardDescription>Members will receive an email with a link to join your organisation.</CardDescription>
|
|
182
|
+
</CardHeader>
|
|
183
|
+
<CardContent className="space-y-4">
|
|
184
|
+
{emails.map((email, i) => (
|
|
185
|
+
<div key={i} className="flex gap-2">
|
|
186
|
+
<div className="flex-1 space-y-1.5">
|
|
187
|
+
<Label className="text-xs text-muted-foreground">Email address</Label>
|
|
188
|
+
<Input
|
|
189
|
+
type="email"
|
|
190
|
+
placeholder="colleague@example.com"
|
|
191
|
+
value={email}
|
|
192
|
+
onChange={(e) => { const next = [...emails]; next[i] = e.target.value; setEmails(next); }}
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="space-y-1.5">
|
|
196
|
+
<Label className="text-xs text-muted-foreground">Role</Label>
|
|
197
|
+
<Select defaultValue="member">
|
|
198
|
+
<option value="member">Member</option>
|
|
199
|
+
<option value="operator">Operator</option>
|
|
200
|
+
<option value="supervisor">Supervisor</option>
|
|
201
|
+
<option value="admin">Admin</option>
|
|
202
|
+
</Select>
|
|
203
|
+
</div>
|
|
204
|
+
{emails.length > 1 && (
|
|
205
|
+
<button onClick={() => setEmails(emails.filter((_, j) => j !== i))} className="mt-6 text-muted-foreground hover:text-destructive">
|
|
206
|
+
<X className="h-4 w-4" />
|
|
207
|
+
</button>
|
|
208
|
+
)}
|
|
209
|
+
</div>
|
|
210
|
+
))}
|
|
211
|
+
<Button variant="outline" size="sm" className="w-full border-dashed" onClick={() => setEmails([...emails, ''])}>
|
|
212
|
+
<Plus className="h-3.5 w-3.5 mr-1.5" />Add another
|
|
213
|
+
</Button>
|
|
214
|
+
</CardContent>
|
|
215
|
+
<CardFooter className="justify-end gap-2">
|
|
216
|
+
<Button variant="outline" size="sm">Cancel</Button>
|
|
217
|
+
<Button size="sm" onClick={() => { setSent(true); setEmails(['']); }}>
|
|
218
|
+
<Mail className="h-4 w-4 mr-1.5" />Send invitations
|
|
219
|
+
</Button>
|
|
220
|
+
</CardFooter>
|
|
221
|
+
</Card>
|
|
222
|
+
|
|
223
|
+
<Card>
|
|
224
|
+
<CardHeader>
|
|
225
|
+
<CardTitle>Invite via link</CardTitle>
|
|
226
|
+
<CardDescription>Anyone with this link can join as a Member. Regenerate to invalidate the old link.</CardDescription>
|
|
227
|
+
</CardHeader>
|
|
228
|
+
<CardContent className="space-y-3">
|
|
229
|
+
<div className="flex gap-2">
|
|
230
|
+
<Input value="https://tad.io/invite/track-any-device/abc123xyz" readOnly className="text-xs font-mono" />
|
|
231
|
+
<Button variant="outline" size="icon"><Copy className="h-4 w-4" /></Button>
|
|
232
|
+
</div>
|
|
233
|
+
<Button variant="outline" size="sm" className="w-full">
|
|
234
|
+
<Link2 className="h-3.5 w-3.5 mr-1.5" />Regenerate link
|
|
235
|
+
</Button>
|
|
236
|
+
</CardContent>
|
|
237
|
+
</Card>
|
|
238
|
+
</div>
|
|
239
|
+
</LayoutResolved>
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function RolesPage({ layout, roles }: { layout: LayoutName; roles: Role[] }) {
|
|
244
|
+
return (
|
|
245
|
+
<LayoutResolved layout={layout}>
|
|
246
|
+
<div className="p-6 max-w-4xl mx-auto space-y-6">
|
|
247
|
+
<div className="flex items-center justify-between">
|
|
248
|
+
<h1 className="text-xl font-semibold">Roles</h1>
|
|
249
|
+
<Button size="sm"><Plus className="h-4 w-4 mr-1.5" />Create role</Button>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div className="grid grid-cols-2 gap-4">
|
|
253
|
+
{roles.map((role) => (
|
|
254
|
+
<Card key={role.name}>
|
|
255
|
+
<CardHeader className="pb-3">
|
|
256
|
+
<div className="flex items-center justify-between">
|
|
257
|
+
<div className="flex items-center gap-2">
|
|
258
|
+
<Badge variant="outline" className={`text-xs ${role.color}`}>{role.name}</Badge>
|
|
259
|
+
<span className="text-xs text-muted-foreground">{role.count} member{role.count !== 1 ? 's' : ''}</span>
|
|
260
|
+
</div>
|
|
261
|
+
<Button variant="ghost" size="icon" className="h-7 w-7"><Settings className="h-3.5 w-3.5" /></Button>
|
|
262
|
+
</div>
|
|
263
|
+
<p className="text-sm text-muted-foreground mt-1">{role.desc}</p>
|
|
264
|
+
</CardHeader>
|
|
265
|
+
<CardContent>
|
|
266
|
+
<p className="text-xs font-medium text-muted-foreground mb-2">Permissions</p>
|
|
267
|
+
<ul className="space-y-1">
|
|
268
|
+
{role.perms.map((p) => (
|
|
269
|
+
<li key={p} className="flex items-center gap-1.5 text-xs">
|
|
270
|
+
<Check className="h-3 w-3 text-green-600 shrink-0" />{p}
|
|
271
|
+
</li>
|
|
272
|
+
))}
|
|
273
|
+
</ul>
|
|
274
|
+
</CardContent>
|
|
275
|
+
</Card>
|
|
276
|
+
))}
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</LayoutResolved>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function PermissionsTogglePage({ layout, sections }: { layout: LayoutName; sections: PermissionSection[] }) {
|
|
284
|
+
return (
|
|
285
|
+
<LayoutResolved layout={layout}>
|
|
286
|
+
<div className="p-6 max-w-4xl mx-auto space-y-6">
|
|
287
|
+
<div className="flex items-center justify-between">
|
|
288
|
+
<h1 className="text-xl font-semibold">Permissions</h1>
|
|
289
|
+
<Button size="sm">Save changes</Button>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<Card>
|
|
293
|
+
<CardContent className="p-0">
|
|
294
|
+
<Table>
|
|
295
|
+
<TableHeader>
|
|
296
|
+
<TableRow>
|
|
297
|
+
<TableHead className="w-2/5">Permission</TableHead>
|
|
298
|
+
<TableHead className="text-center">Admin</TableHead>
|
|
299
|
+
<TableHead className="text-center">Supervisor</TableHead>
|
|
300
|
+
<TableHead className="text-center">Operator</TableHead>
|
|
301
|
+
<TableHead className="text-center">Member</TableHead>
|
|
302
|
+
</TableRow>
|
|
303
|
+
</TableHeader>
|
|
304
|
+
<TableBody>
|
|
305
|
+
{sections.map(({ section, rows }) => (
|
|
306
|
+
<>
|
|
307
|
+
<TableRow key={section} className="bg-muted/30">
|
|
308
|
+
<TableCell colSpan={5} className="py-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground">{section}</TableCell>
|
|
309
|
+
</TableRow>
|
|
310
|
+
{rows.map((row) => (
|
|
311
|
+
<TableRow key={row.label}>
|
|
312
|
+
<TableCell className="text-sm">{row.label}</TableCell>
|
|
313
|
+
{(['admin', 'supervisor', 'operator', 'member'] as const).map((role) => (
|
|
314
|
+
<TableCell key={role} className="text-center">
|
|
315
|
+
<div className="flex justify-center">
|
|
316
|
+
<Switch defaultChecked={row[role]} disabled={role === 'admin'} />
|
|
317
|
+
</div>
|
|
318
|
+
</TableCell>
|
|
319
|
+
))}
|
|
320
|
+
</TableRow>
|
|
321
|
+
))}
|
|
322
|
+
</>
|
|
323
|
+
))}
|
|
324
|
+
</TableBody>
|
|
325
|
+
</Table>
|
|
326
|
+
</CardContent>
|
|
327
|
+
</Card>
|
|
328
|
+
</div>
|
|
329
|
+
</LayoutResolved>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function TeamInfoPage({ layout, members, roles }: { layout: LayoutName; members: Member[]; roles: Role[] }) {
|
|
334
|
+
const active = members.filter((m) => m.status === 'Active').length;
|
|
335
|
+
const pending = members.filter((m) => m.status === 'Invited').length;
|
|
336
|
+
const twofa = members.filter((m) => m.twofa).length;
|
|
337
|
+
const twoFaPct = Math.round((twofa / members.length) * 100);
|
|
338
|
+
|
|
339
|
+
return (
|
|
340
|
+
<LayoutResolved layout={layout}>
|
|
341
|
+
<div className="p-6 max-w-3xl mx-auto space-y-6">
|
|
342
|
+
<h1 className="text-xl font-semibold">Team</h1>
|
|
343
|
+
|
|
344
|
+
<div className="grid grid-cols-4 gap-4">
|
|
345
|
+
{[
|
|
346
|
+
{ label: 'Total members', value: String(members.length), sub: 'Across all roles' },
|
|
347
|
+
{ label: 'Active', value: String(active), sub: 'Signed in recently' },
|
|
348
|
+
{ label: 'Pending invite', value: String(pending), sub: 'Awaiting acceptance' },
|
|
349
|
+
{ label: 'Seats used', value: `${members.length}/10`, sub: `${10 - members.length} seats remaining` },
|
|
350
|
+
].map(({ label, value, sub }) => (
|
|
351
|
+
<Card key={label}>
|
|
352
|
+
<CardContent className="p-4 text-center">
|
|
353
|
+
<p className="text-2xl font-bold text-primary">{value}</p>
|
|
354
|
+
<p className="text-sm font-medium mt-0.5">{label}</p>
|
|
355
|
+
<p className="text-xs text-muted-foreground mt-0.5">{sub}</p>
|
|
356
|
+
</CardContent>
|
|
357
|
+
</Card>
|
|
358
|
+
))}
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<Card>
|
|
362
|
+
<CardHeader><CardTitle>Role distribution</CardTitle></CardHeader>
|
|
363
|
+
<CardContent className="space-y-3">
|
|
364
|
+
{roles.map(({ name, color, count }) => (
|
|
365
|
+
<div key={name} className="flex items-center justify-between">
|
|
366
|
+
<div className="flex items-center gap-2">
|
|
367
|
+
<Badge variant="outline" className={`text-xs ${color}`}>{name}</Badge>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="flex items-center gap-3 flex-1 max-w-xs ml-4">
|
|
370
|
+
<div className="flex-1 rounded-full h-1.5 bg-muted overflow-hidden">
|
|
371
|
+
<div className="h-full bg-primary rounded-full" style={{ width: `${(count / members.length) * 100}%` }} />
|
|
372
|
+
</div>
|
|
373
|
+
<span className="text-xs text-muted-foreground w-16 text-right">{count} of {members.length}</span>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
))}
|
|
377
|
+
</CardContent>
|
|
378
|
+
</Card>
|
|
379
|
+
|
|
380
|
+
<Card>
|
|
381
|
+
<CardHeader>
|
|
382
|
+
<CardTitle>2FA compliance</CardTitle>
|
|
383
|
+
<CardDescription>Members with two-factor authentication enabled.</CardDescription>
|
|
384
|
+
</CardHeader>
|
|
385
|
+
<CardContent>
|
|
386
|
+
<div className="flex items-center gap-4">
|
|
387
|
+
<div className="text-3xl font-bold text-primary">{twoFaPct}%</div>
|
|
388
|
+
<div className="flex-1">
|
|
389
|
+
<div className="rounded-full h-2.5 bg-muted overflow-hidden">
|
|
390
|
+
<div className="h-full bg-primary rounded-full" style={{ width: `${twoFaPct}%` }} />
|
|
391
|
+
</div>
|
|
392
|
+
<p className="text-xs text-muted-foreground mt-1.5">{twofa} of {members.length} members have 2FA enabled</p>
|
|
393
|
+
</div>
|
|
394
|
+
<Button variant="outline" size="sm"><Shield className="h-3.5 w-3.5 mr-1.5" />Enforce 2FA</Button>
|
|
395
|
+
</div>
|
|
396
|
+
</CardContent>
|
|
397
|
+
</Card>
|
|
398
|
+
|
|
399
|
+
<Card>
|
|
400
|
+
<CardHeader><CardTitle>Import members</CardTitle><CardDescription>Bulk import team members from a CSV file.</CardDescription></CardHeader>
|
|
401
|
+
<CardContent className="space-y-3">
|
|
402
|
+
<div className="rounded-lg border-2 border-dashed border-border p-8 text-center">
|
|
403
|
+
<Users className="h-8 w-8 text-muted-foreground mx-auto mb-2" />
|
|
404
|
+
<p className="text-sm font-medium">Drop your CSV here</p>
|
|
405
|
+
<p className="text-xs text-muted-foreground mt-0.5">or click to browse — max 500 rows</p>
|
|
406
|
+
<Button variant="outline" size="sm" className="mt-3">Browse file</Button>
|
|
407
|
+
</div>
|
|
408
|
+
<a href="#" className="text-xs text-primary underline">Download sample CSV template</a>
|
|
409
|
+
</CardContent>
|
|
410
|
+
</Card>
|
|
411
|
+
</div>
|
|
412
|
+
</LayoutResolved>
|
|
413
|
+
);
|
|
414
|
+
}
|