291 lines
11 KiB
Vue
291 lines
11 KiB
Vue
<template>
|
|
<div class="min-h-screen bg-background">
|
|
<NuxtLayout name="default">
|
|
<main class="container mx-auto px-4 py-8">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold">Users</h1>
|
|
<p class="text-muted-foreground">Manage user accounts and access</p>
|
|
</div>
|
|
<Button @click="showCreateDialog = true">
|
|
<UserPlus class="mr-2 h-4 w-4" />
|
|
New User
|
|
</Button>
|
|
</div>
|
|
|
|
<div class="border rounded-lg">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>Email</TableHead>
|
|
<TableHead>Roles</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead class="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<TableRow v-if="loading">
|
|
<TableCell :colspan="5" class="text-center py-8">
|
|
<div class="flex items-center justify-center">
|
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow v-else-if="users.length === 0">
|
|
<TableCell :colspan="5" class="text-center py-8 text-muted-foreground">
|
|
No users found. Create your first user to get started.
|
|
</TableCell>
|
|
</TableRow>
|
|
<TableRow v-else v-for="user in users" :key="user.id" class="cursor-pointer hover:bg-muted/50" @click="navigateTo(`/setup/users/${user.id}`)">
|
|
<TableCell class="font-medium">{{ getUserName(user) }}</TableCell>
|
|
<TableCell>{{ user.email }}</TableCell>
|
|
<TableCell>
|
|
<div class="flex gap-1 flex-wrap">
|
|
<Badge v-for="role in user.roles" :key="role.id" variant="secondary">
|
|
{{ role.name }}
|
|
</Badge>
|
|
<span v-if="!user.roles || user.roles.length === 0" class="text-muted-foreground text-sm">
|
|
No roles
|
|
</span>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{{ formatDate(user.createdAt) }}</TableCell>
|
|
<TableCell class="text-right" @click.stop>
|
|
<div class="flex items-center justify-end gap-1">
|
|
<Button variant="ghost" size="icon" @click="navigateTo(`/setup/users/${user.id}`)">
|
|
<Eye class="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" @click="openEditDialog(user)">
|
|
<Edit class="h-4 w-4" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" @click="openDeleteDialog(user)">
|
|
<Trash2 class="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<!-- Create User Dialog -->
|
|
<Dialog v-model:open="showCreateDialog">
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Create New User</DialogTitle>
|
|
<DialogDescription>
|
|
Add a new user to the system
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div class="space-y-4">
|
|
<div class="space-y-2">
|
|
<Label for="email">Email</Label>
|
|
<Input id="email" v-model="newUser.email" type="email" placeholder="user@example.com" />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="password">Password</Label>
|
|
<Input id="password" v-model="newUser.password" type="password" placeholder="••••••••" />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="firstName">First Name (Optional)</Label>
|
|
<Input id="firstName" v-model="newUser.firstName" placeholder="John" />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="lastName">Last Name (Optional)</Label>
|
|
<Input id="lastName" v-model="newUser.lastName" placeholder="Doe" />
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="showCreateDialog = false">Cancel</Button>
|
|
<Button @click="createUser" :disabled="!newUser.email || !newUser.password">
|
|
Create User
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<!-- Edit User Dialog -->
|
|
<Dialog v-model:open="showEditDialog">
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Edit User</DialogTitle>
|
|
<DialogDescription>
|
|
Update user information
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div class="space-y-4">
|
|
<div class="space-y-2">
|
|
<Label for="edit-email">Email</Label>
|
|
<Input id="edit-email" v-model="editUser.email" type="email" placeholder="user@example.com" />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="edit-password">Password (leave blank to keep current)</Label>
|
|
<Input id="edit-password" v-model="editUser.password" type="password" placeholder="••••••••" />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="edit-firstName">First Name</Label>
|
|
<Input id="edit-firstName" v-model="editUser.firstName" placeholder="John" />
|
|
</div>
|
|
<div class="space-y-2">
|
|
<Label for="edit-lastName">Last Name</Label>
|
|
<Input id="edit-lastName" v-model="editUser.lastName" placeholder="Doe" />
|
|
</div>
|
|
</div>
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="showEditDialog = false">Cancel</Button>
|
|
<Button @click="updateUser" :disabled="!editUser.email">
|
|
Update User
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<!-- Delete Confirmation Dialog -->
|
|
<Dialog v-model:open="showDeleteDialog">
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Delete User</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete this user? This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="showDeleteDialog = false">Cancel</Button>
|
|
<Button variant="destructive" @click="deleteUser">
|
|
Delete User
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</main>
|
|
</NuxtLayout>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
import { Button } from '~/components/ui/button';
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '~/components/ui/table';
|
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '~/components/ui/dialog';
|
|
import { Input } from '~/components/ui/input';
|
|
import { Label } from '~/components/ui/label';
|
|
import { Badge } from '~/components/ui/badge';
|
|
import { UserPlus, Eye, Edit, Trash2 } from 'lucide-vue-next';
|
|
|
|
|
|
const { api } = useApi();
|
|
const { toast } = useToast();
|
|
|
|
const loading = ref(true);
|
|
const users = ref<any[]>([]);
|
|
const showCreateDialog = ref(false);
|
|
const showEditDialog = ref(false);
|
|
const showDeleteDialog = ref(false);
|
|
const newUser = ref({
|
|
email: '',
|
|
password: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
});
|
|
const editUser = ref({
|
|
id: '',
|
|
email: '',
|
|
password: '',
|
|
firstName: '',
|
|
lastName: '',
|
|
});
|
|
const userToDelete = ref<any>(null);
|
|
|
|
const loadUsers = async () => {
|
|
try {
|
|
loading.value = true;
|
|
const response = await api.get('/setup/users');
|
|
users.value = response || [];
|
|
} catch (error: any) {
|
|
console.error('Failed to load users:', error);
|
|
toast.error('Failed to load users');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
};
|
|
|
|
const createUser = async () => {
|
|
try {
|
|
await api.post('/setup/users', newUser.value);
|
|
toast.success('User created successfully');
|
|
showCreateDialog.value = false;
|
|
newUser.value = { email: '', password: '', firstName: '', lastName: '' };
|
|
await loadUsers();
|
|
} catch (error: any) {
|
|
console.error('Failed to create user:', error);
|
|
toast.error(error.message || 'Failed to create user');
|
|
}
|
|
};
|
|
|
|
const openEditDialog = (user: any) => {
|
|
editUser.value = {
|
|
id: user.id,
|
|
email: user.email,
|
|
password: '',
|
|
firstName: user.firstName || '',
|
|
lastName: user.lastName || '',
|
|
};
|
|
showEditDialog.value = true;
|
|
};
|
|
|
|
const updateUser = async () => {
|
|
try {
|
|
const payload: any = {
|
|
email: editUser.value.email,
|
|
firstName: editUser.value.firstName,
|
|
lastName: editUser.value.lastName,
|
|
};
|
|
if (editUser.value.password) {
|
|
payload.password = editUser.value.password;
|
|
}
|
|
await api.patch(`/setup/users/${editUser.value.id}`, payload);
|
|
toast.success('User updated successfully');
|
|
showEditDialog.value = false;
|
|
await loadUsers();
|
|
} catch (error: any) {
|
|
console.error('Failed to update user:', error);
|
|
toast.error(error.message || 'Failed to update user');
|
|
}
|
|
};
|
|
|
|
const openDeleteDialog = (user: any) => {
|
|
userToDelete.value = user;
|
|
showDeleteDialog.value = true;
|
|
};
|
|
|
|
const deleteUser = async () => {
|
|
try {
|
|
await api.delete(`/setup/users/${userToDelete.value.id}`);
|
|
toast.success('User deleted successfully');
|
|
showDeleteDialog.value = false;
|
|
userToDelete.value = null;
|
|
await loadUsers();
|
|
} catch (error: any) {
|
|
console.error('Failed to delete user:', error);
|
|
toast.error(error.message || 'Failed to delete user');
|
|
}
|
|
};
|
|
|
|
const getUserName = (user: any) => {
|
|
if (user.firstName || user.lastName) {
|
|
return [user.firstName, user.lastName].filter(Boolean).join(' ');
|
|
}
|
|
return user.email;
|
|
};
|
|
|
|
const formatDate = (date: string) => {
|
|
if (!date) return 'N/A';
|
|
return new Date(date).toLocaleDateString();
|
|
};
|
|
|
|
onMounted(() => {
|
|
loadUsers();
|
|
});
|
|
</script>
|