Init Repo

This commit is contained in:
2026-02-28 03:22:04 +08:00
commit de59b57ee7
883 changed files with 156857 additions and 0 deletions
@@ -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>