Init Repo
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-lg font-medium">All Users</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Manage user roles and permissions
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Input
|
||||
v-model="searchQuery"
|
||||
placeholder="Search users..."
|
||||
class="w-64"
|
||||
/>
|
||||
<Select v-model="roleFilter">
|
||||
<SelectTrigger class="w-40">
|
||||
<SelectValue placeholder="Filter by role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Roles</SelectItem>
|
||||
<SelectItem value="director">Director</SelectItem>
|
||||
<SelectItem value="coordinator">Coordinator</SelectItem>
|
||||
<SelectItem value="artist">Artist</SelectItem>
|
||||
<SelectItem value="developer">Developer</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select v-model="approvalFilter">
|
||||
<SelectTrigger class="w-40">
|
||||
<SelectValue placeholder="Filter by status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Status</SelectItem>
|
||||
<SelectItem value="approved">Approved</SelectItem>
|
||||
<SelectItem value="pending">Pending</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select v-model="adminFilter">
|
||||
<SelectTrigger class="w-40">
|
||||
<SelectValue placeholder="Filter by admin" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Users</SelectItem>
|
||||
<SelectItem value="admin">Admin Only</SelectItem>
|
||||
<SelectItem value="non-admin">Non-Admin</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
<Button variant="ghost" size="sm" @click="toggleSort('name')" class="h-8 px-2">
|
||||
User
|
||||
<ArrowUpDown class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Button variant="ghost" size="sm" @click="toggleSort('email')" class="h-8 px-2">
|
||||
Email
|
||||
<ArrowUpDown class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Button variant="ghost" size="sm" @click="toggleSort('role')" class="h-8 px-2">
|
||||
Role
|
||||
<ArrowUpDown class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead>Admin</TableHead>
|
||||
<TableHead>
|
||||
<Button variant="ghost" size="sm" @click="toggleSort('status')" class="h-8 px-2">
|
||||
Status
|
||||
<ArrowUpDown class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<Button variant="ghost" size="sm" @click="toggleSort('joined')" class="h-8 px-2">
|
||||
Joined
|
||||
<ArrowUpDown class="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</TableHead>
|
||||
<TableHead class="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="user in sortedAndFilteredUsers" :key="user.id">
|
||||
<TableCell>
|
||||
<div class="flex items-center space-x-3">
|
||||
<Avatar class="h-8 w-8">
|
||||
<AvatarImage
|
||||
v-if="user.avatar_url"
|
||||
:src="getAvatarUrl(user.avatar_url, user.id)"
|
||||
/>
|
||||
<AvatarImage
|
||||
v-else
|
||||
:src="`https://api.dicebear.com/7.x/initials/svg?seed=${user.first_name} ${user.last_name}`"
|
||||
/>
|
||||
<AvatarFallback>{{ getUserInitials(user) }}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<div class="font-medium">
|
||||
{{ user.first_name }} {{ user.last_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div class="text-sm text-muted-foreground">
|
||||
{{ user.email }}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{{ formatRole(user.role) }}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge v-if="user.is_admin" variant="destructive">Admin</Badge>
|
||||
<span v-else class="text-sm text-muted-foreground">—</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="user.is_approved ? 'default' : 'secondary'">
|
||||
{{ user.is_approved ? "Approved" : "Pending" }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{{ formatDate(user.created_at) }}
|
||||
</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal class="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem @click="handleEditUser(user)">
|
||||
<Edit class="h-4 w-4 mr-2" />
|
||||
Edit User
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem @click="handleResetPassword(user)">
|
||||
<Key class="h-4 w-4 mr-2" />
|
||||
Reset Password
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
v-if="!user.is_approved"
|
||||
@click="handleApproveUser(user.id)"
|
||||
>
|
||||
<Check class="h-4 w-4 mr-2" />
|
||||
Approve User
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="!user.is_admin"
|
||||
@click="handleAdminPermissionChange(user.id, true)"
|
||||
:disabled="isUpdatingAdminPermission === user.id"
|
||||
>
|
||||
<UserCheck class="h-4 w-4 mr-2" />
|
||||
Grant Admin Permission
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="user.is_admin"
|
||||
@click="handleAdminPermissionChange(user.id, false)"
|
||||
:disabled="isUpdatingAdminPermission === user.id"
|
||||
class="text-orange-600 focus:text-orange-600"
|
||||
>
|
||||
<UserX class="h-4 w-4 mr-2" />
|
||||
Revoke Admin Permission
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
class="text-destructive"
|
||||
@click="handleDeleteUser(user)"
|
||||
:disabled="!!(currentUser && user.id === currentUser.id)"
|
||||
>
|
||||
<Trash2 class="h-4 w-4 mr-2" />
|
||||
Delete User
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { MoreHorizontal, Eye, Check, UserX, UserCheck, Edit, Key, Trash2, ArrowUpDown } from "lucide-vue-next";
|
||||
import type { User } from "@/types/auth";
|
||||
|
||||
interface Props {
|
||||
users: User[];
|
||||
currentUser?: User | null;
|
||||
isUpdatingRole?: number;
|
||||
isUpdatingAdminPermission?: number;
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: "roleChange", userId: number, role: User["role"]): void;
|
||||
(e: "adminPermissionChange", userId: number, isAdmin: boolean): void;
|
||||
(e: "approveUser", userId: number): void;
|
||||
(e: "editUser", user: User): void;
|
||||
(e: "resetPassword", user: User): void;
|
||||
(e: "deleteUser", user: User): void;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
const searchQuery = ref("");
|
||||
const roleFilter = ref("all");
|
||||
const approvalFilter = ref("all");
|
||||
const adminFilter = ref("all");
|
||||
const sortColumn = ref<string | null>(null);
|
||||
const sortDirection = ref<'asc' | 'desc'>('asc');
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
let filtered = props.users;
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
filtered = filtered.filter(
|
||||
(user) =>
|
||||
user.first_name.toLowerCase().includes(query) ||
|
||||
user.last_name.toLowerCase().includes(query) ||
|
||||
user.email.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by role
|
||||
if (roleFilter.value !== "all") {
|
||||
filtered = filtered.filter((user) => user.role === roleFilter.value);
|
||||
}
|
||||
|
||||
// Filter by approval status
|
||||
if (approvalFilter.value !== "all") {
|
||||
filtered = filtered.filter((user) =>
|
||||
approvalFilter.value === "approved" ? user.is_approved : !user.is_approved
|
||||
);
|
||||
}
|
||||
|
||||
// Filter by admin permission
|
||||
if (adminFilter.value !== "all") {
|
||||
filtered = filtered.filter((user) =>
|
||||
adminFilter.value === "admin" ? user.is_admin : !user.is_admin
|
||||
);
|
||||
}
|
||||
|
||||
return filtered;
|
||||
});
|
||||
|
||||
const sortedAndFilteredUsers = computed(() => {
|
||||
const users = [...filteredUsers.value];
|
||||
|
||||
if (!sortColumn.value) {
|
||||
return users;
|
||||
}
|
||||
|
||||
return users.sort((a, b) => {
|
||||
let aValue: any;
|
||||
let bValue: any;
|
||||
|
||||
switch (sortColumn.value) {
|
||||
case 'name':
|
||||
aValue = `${a.first_name} ${a.last_name}`.toLowerCase();
|
||||
bValue = `${b.first_name} ${b.last_name}`.toLowerCase();
|
||||
break;
|
||||
case 'email':
|
||||
aValue = a.email.toLowerCase();
|
||||
bValue = b.email.toLowerCase();
|
||||
break;
|
||||
case 'role':
|
||||
aValue = a.role;
|
||||
bValue = b.role;
|
||||
break;
|
||||
case 'status':
|
||||
aValue = a.is_approved ? 1 : 0;
|
||||
bValue = b.is_approved ? 1 : 0;
|
||||
break;
|
||||
case 'joined':
|
||||
aValue = new Date(a.created_at).getTime();
|
||||
bValue = new Date(b.created_at).getTime();
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (aValue < bValue) {
|
||||
return sortDirection.value === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return sortDirection.value === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
const toggleSort = (column: string) => {
|
||||
if (sortColumn.value === column) {
|
||||
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortColumn.value = column;
|
||||
sortDirection.value = 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
const getUserInitials = (user: User) => {
|
||||
if (!user || !user.first_name || !user.last_name) {
|
||||
return '??';
|
||||
}
|
||||
return `${user.first_name.charAt(0)}${user.last_name.charAt(0)}`.toUpperCase();
|
||||
};
|
||||
|
||||
const getAvatarUrl = (url: string | null | undefined, userId?: number) => {
|
||||
if (!url) return ''
|
||||
// If it's already a full URL, return it
|
||||
if (url.startsWith('http')) return url
|
||||
// Use direct static file serving
|
||||
const cleanUrl = url.replace(/^backend[\/\\]/, '').replace(/\\/g, '/')
|
||||
return `/${cleanUrl}`
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
};
|
||||
|
||||
const formatRole = (role: string) => {
|
||||
if (!role || typeof role !== 'string') {
|
||||
return 'Unknown';
|
||||
}
|
||||
return role.charAt(0).toUpperCase() + role.slice(1);
|
||||
};
|
||||
|
||||
const handleAdminPermissionChange = (userId: number, isAdmin: boolean) => {
|
||||
emit("adminPermissionChange", userId, isAdmin);
|
||||
};
|
||||
|
||||
const handleApproveUser = (userId: number) => {
|
||||
emit("approveUser", userId);
|
||||
};
|
||||
|
||||
const handleEditUser = (user: User) => {
|
||||
emit("editUser", user);
|
||||
};
|
||||
|
||||
const handleResetPassword = (user: User) => {
|
||||
emit("resetPassword", user);
|
||||
};
|
||||
|
||||
const handleDeleteUser = (user: User) => {
|
||||
emit("deleteUser", user);
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user