feat: allow admins and coordinators to also be drivers

Add a "Driver" checkbox column to the User Management page. Checking it
creates a linked Driver record so the user appears in the drivers list,
can be assigned events, and enrolled for GPS tracking — without changing
their primary role. The DRIVER role checkbox is auto-checked and disabled
since being a driver is inherent to that role. Promoting a user from
DRIVER to Admin/Coordinator preserves their driver record.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 21:19:08 +01:00
parent ec7c5a6802
commit 42bab25766
3 changed files with 94 additions and 29 deletions

View File

@@ -1,7 +1,7 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import toast from 'react-hot-toast';
import { api } from '@/lib/api';
import { Check, X, UserCheck, UserX, Shield, Trash2 } from 'lucide-react';
import { Check, X, UserCheck, UserX, Shield, Trash2, Car } from 'lucide-react';
import { useState } from 'react';
import { Loading } from '@/components/Loading';
@@ -12,6 +12,10 @@ interface User {
role: string;
isApproved: boolean;
createdAt: string;
driver?: {
id: string;
deletedAt: string | null;
} | null;
}
export function UserList() {
@@ -72,6 +76,21 @@ export function UserList() {
},
});
const toggleDriverMutation = useMutation({
mutationFn: async ({ userId, isAlsoDriver }: { userId: string; isAlsoDriver: boolean }) => {
await api.patch(`/users/${userId}`, { isAlsoDriver });
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
queryClient.invalidateQueries({ queryKey: ['drivers'] });
toast.success('Driver status updated');
},
onError: (error: any) => {
console.error('[USERS] Failed to toggle driver status:', error);
toast.error(error.response?.data?.message || 'Failed to update driver status');
},
});
const handleRoleChange = (userId: string, newRole: string) => {
if (confirm(`Change user role to ${newRole}?`)) {
changeRoleMutation.mutate({ userId, role: newRole });
@@ -201,6 +220,9 @@ export function UserList() {
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">
Role
</th>
<th className="px-6 py-3 text-center text-xs font-medium text-muted-foreground uppercase">
Driver
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase">
Status
</th>
@@ -210,7 +232,11 @@ export function UserList() {
</tr>
</thead>
<tbody className="bg-card divide-y divide-border">
{approvedUsers.map((user) => (
{approvedUsers.map((user) => {
const hasDriver = !!(user.driver && !user.driver.deletedAt);
const isDriverRole = user.role === 'DRIVER';
return (
<tr key={user.id} className="hover:bg-accent transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-foreground">
{user.name || 'Unknown User'}
@@ -232,6 +258,24 @@ export function UserList() {
{user.role}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-center">
<label className="inline-flex items-center gap-1.5 cursor-pointer" title={
isDriverRole
? 'Inherent to DRIVER role'
: hasDriver
? 'Remove from driver list'
: 'Add to driver list'
}>
<input
type="checkbox"
checked={hasDriver || isDriverRole}
disabled={isDriverRole || toggleDriverMutation.isPending}
onChange={() => toggleDriverMutation.mutate({ userId: user.id, isAlsoDriver: !hasDriver })}
className="h-4 w-4 rounded border-gray-300 text-primary focus:ring-primary disabled:opacity-50"
/>
{hasDriver && <Car className="h-3.5 w-3.5 text-muted-foreground" />}
</label>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-green-600 dark:text-green-400 font-medium">
<Check className="h-4 w-4 inline mr-1" />
Active
@@ -257,7 +301,8 @@ export function UserList() {
</div>
</td>
</tr>
))}
);
})}
</tbody>
</table>
</div>