membership page
This commit is contained in:
185
src/api/membershipService.ts
Normal file
185
src/api/membershipService.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { fetchAPIEvent } from '$api/apiService';
|
||||
import type { User } from '$models/user';
|
||||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export interface MembershipFilter {
|
||||
username?: string;
|
||||
role_name?: string;
|
||||
role_id?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
sort_by?: string;
|
||||
sort_desc?: boolean;
|
||||
}
|
||||
|
||||
export interface PaginatedUsers {
|
||||
users: User[];
|
||||
pagination: {
|
||||
page: number;
|
||||
page_size: number;
|
||||
total: number;
|
||||
total_pages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
username?: string;
|
||||
password?: string;
|
||||
roleId?: string;
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const membershipService = {
|
||||
async getUsers(event: RequestEvent, filter?: MembershipFilter): Promise<User[]> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (filter) {
|
||||
if (filter.username) queryParams.append('username', filter.username);
|
||||
if (filter.role_name) queryParams.append('role_name', filter.role_name);
|
||||
if (filter.role_id) queryParams.append('role_id', filter.role_id);
|
||||
if (filter.page) queryParams.append('page', filter.page.toString());
|
||||
if (filter.page_size) queryParams.append('page_size', filter.page_size.toString());
|
||||
if (filter.sort_by) queryParams.append('sort_by', filter.sort_by);
|
||||
if (filter.sort_desc !== undefined)
|
||||
queryParams.append('sort_desc', filter.sort_desc.toString());
|
||||
}
|
||||
|
||||
const endpoint = `/membership${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
||||
return await fetchAPIEvent(event, endpoint);
|
||||
},
|
||||
|
||||
async getUsersPaginated(event: RequestEvent, filter?: MembershipFilter): Promise<PaginatedUsers> {
|
||||
const users = await this.getUsers(event, filter);
|
||||
|
||||
const page = filter?.page || 1;
|
||||
const pageSize = filter?.page_size || 10;
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
|
||||
const paginatedUsers = users.slice(startIndex, endIndex);
|
||||
const totalPages = Math.ceil(users.length / pageSize);
|
||||
|
||||
return {
|
||||
users: paginatedUsers,
|
||||
pagination: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
total: users.length,
|
||||
total_pages: totalPages
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
async getUser(event: RequestEvent, userId: string): Promise<User> {
|
||||
return await fetchAPIEvent(event, `/membership/${userId}`);
|
||||
},
|
||||
|
||||
async createUser(event: RequestEvent, userData: CreateUserRequest): Promise<User> {
|
||||
return await fetchAPIEvent(event, '/membership', 'POST', userData);
|
||||
},
|
||||
|
||||
async updateUser(
|
||||
event: RequestEvent,
|
||||
userId: string,
|
||||
userData: UpdateUserRequest
|
||||
): Promise<User> {
|
||||
return await fetchAPIEvent(event, `/membership/${userId}`, 'PUT', userData);
|
||||
},
|
||||
|
||||
async deleteUser(event: RequestEvent, userId: string): Promise<void> {
|
||||
await fetchAPIEvent(event, `/membership/${userId}`, 'DELETE');
|
||||
},
|
||||
|
||||
async getRoles(event: RequestEvent): Promise<Role[]> {
|
||||
return await fetchAPIEvent(event, '/membership/roles');
|
||||
}
|
||||
};
|
||||
|
||||
// Client-side service for browser usage (not currently used)
|
||||
export const membershipClientService = {
|
||||
async getUsers(filter?: MembershipFilter): Promise<User[]> {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (filter) {
|
||||
if (filter.username) queryParams.append('username', filter.username);
|
||||
if (filter.role_name) queryParams.append('role_name', filter.role_name);
|
||||
if (filter.role_id) queryParams.append('role_id', filter.role_id);
|
||||
if (filter.page) queryParams.append('page', filter.page.toString());
|
||||
if (filter.page_size) queryParams.append('page_size', filter.page_size.toString());
|
||||
if (filter.sort_by) queryParams.append('sort_by', filter.sort_by);
|
||||
if (filter.sort_desc !== undefined)
|
||||
queryParams.append('sort_desc', filter.sort_desc.toString());
|
||||
}
|
||||
|
||||
const endpoint = `/membership${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
|
||||
const response = await fetch(endpoint);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch users: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async createUser(userData: CreateUserRequest): Promise<User> {
|
||||
const response = await fetch('/membership', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to create user: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async updateUser(userId: string, userData: UpdateUserRequest): Promise<User> {
|
||||
const response = await fetch(`/membership/${userId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to update user: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
async deleteUser(userId: string): Promise<void> {
|
||||
const response = await fetch(`/membership/${userId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to delete user: ${response.statusText}`);
|
||||
}
|
||||
},
|
||||
|
||||
async getRoles(): Promise<Role[]> {
|
||||
const response = await fetch('/membership/roles');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch roles: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
};
|
||||
@@ -110,15 +110,15 @@ export const updateConfig = async (
|
||||
);
|
||||
};
|
||||
|
||||
export const restartService = async (event: RequestEvent, serverId: number) => {
|
||||
export const restartService = async (event: RequestEvent, serverId: string) => {
|
||||
return fetchAPIEvent(event, '/api/restart', 'POST', { serverId });
|
||||
};
|
||||
|
||||
export const startService = async (event: RequestEvent, serverId: number) => {
|
||||
export const startService = async (event: RequestEvent, serverId: string) => {
|
||||
return fetchAPIEvent(event, '/api/start', 'POST', { serverId });
|
||||
};
|
||||
|
||||
export const stopService = async (event: RequestEvent, serverId: number) => {
|
||||
export const stopService = async (event: RequestEvent, serverId: string) => {
|
||||
return fetchAPIEvent(event, '/api/stop', 'POST', { serverId });
|
||||
};
|
||||
|
||||
|
||||
95
src/components/Pagination.svelte
Normal file
95
src/components/Pagination.svelte
Normal file
@@ -0,0 +1,95 @@
|
||||
<script lang="ts">
|
||||
export let currentPage: number = 1;
|
||||
export let totalPages: number = 1;
|
||||
export let onPageChange: (page: number) => void = () => {};
|
||||
|
||||
$: pages = generatePageNumbers(currentPage, totalPages);
|
||||
|
||||
function generatePageNumbers(current: number, total: number): (number | string)[] {
|
||||
if (total <= 7) {
|
||||
return Array.from({ length: total }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
const pages: (number | string)[] = [];
|
||||
|
||||
if (current <= 4) {
|
||||
// Show first 5 pages, then ellipsis, then last page
|
||||
pages.push(...[1, 2, 3, 4, 5]);
|
||||
if (total > 6) pages.push('...');
|
||||
pages.push(total);
|
||||
} else if (current >= total - 3) {
|
||||
// Show first page, ellipsis, then last 5 pages
|
||||
pages.push(1);
|
||||
if (total > 6) pages.push('...');
|
||||
pages.push(...[total - 4, total - 3, total - 2, total - 1, total]);
|
||||
} else {
|
||||
// Show first page, ellipsis, current and neighbors, ellipsis, last page
|
||||
pages.push(1, '...', current - 1, current, current + 1, '...', total);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
function handlePageClick(page: number | string) {
|
||||
if (typeof page === 'number' && page !== currentPage) {
|
||||
onPageChange(page);
|
||||
}
|
||||
}
|
||||
|
||||
function handlePrevious() {
|
||||
if (currentPage > 1) {
|
||||
onPageChange(currentPage - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleNext() {
|
||||
if (currentPage < totalPages) {
|
||||
onPageChange(currentPage + 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-center space-x-1">
|
||||
<!-- Previous button -->
|
||||
<button
|
||||
class="rounded-md px-3 py-2 text-sm font-medium transition-colors {currentPage === 1
|
||||
? 'cursor-not-allowed bg-gray-700 text-gray-500'
|
||||
: 'border border-gray-600 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white'}"
|
||||
disabled={currentPage === 1}
|
||||
onclick={handlePrevious}
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<!-- Page numbers -->
|
||||
{#each pages as page}
|
||||
{#if page === '...'}
|
||||
<span class="px-3 py-2 text-sm text-gray-500">...</span>
|
||||
{:else}
|
||||
<button
|
||||
class="rounded-md px-3 py-2 text-sm font-medium transition-colors {page === currentPage
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'border border-gray-600 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white'}"
|
||||
onclick={() => handlePageClick(page)}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- Next button -->
|
||||
<button
|
||||
class="rounded-md px-3 py-2 text-sm font-medium transition-colors {currentPage === totalPages
|
||||
? 'cursor-not-allowed bg-gray-700 text-gray-500'
|
||||
: 'border border-gray-600 bg-gray-800 text-gray-300 hover:bg-gray-700 hover:text-white'}"
|
||||
disabled={currentPage === totalPages}
|
||||
onclick={handleNext}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Page info -->
|
||||
<div class="mt-2 text-center text-sm text-gray-400">
|
||||
Page {currentPage} of {totalPages}
|
||||
</div>
|
||||
@@ -34,11 +34,11 @@
|
||||
<div class="mt-4 grid grid-cols-2 gap-2 text-sm text-gray-300">
|
||||
<div>
|
||||
<span class="text-gray-500">Track:</span>
|
||||
{server.state.track}
|
||||
{server.state?.track}
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-gray-500">Players:</span>
|
||||
{server.state.playerCount}
|
||||
{server.state?.playerCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,7 @@ interface State {
|
||||
}
|
||||
|
||||
export interface Server {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
status: ServiceStatus;
|
||||
state: State;
|
||||
|
||||
27
src/routes/api/membership/+server.ts
Normal file
27
src/routes/api/membership/+server.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { fetchAPIEvent } from '$api/apiService';
|
||||
import { json, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export async function GET(event: RequestEvent) {
|
||||
try {
|
||||
const { url } = event;
|
||||
const queryParams = url.searchParams.toString();
|
||||
const endpoint = `/membership${queryParams ? `?${queryParams}` : ''}`;
|
||||
|
||||
const users = await fetchAPIEvent(event, endpoint);
|
||||
return json(users);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch users:', error);
|
||||
return json({ error: 'Failed to fetch users' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(event: RequestEvent) {
|
||||
try {
|
||||
const userData = await event.request.json();
|
||||
const user = await fetchAPIEvent(event, '/membership', 'POST', userData);
|
||||
return json(user);
|
||||
} catch (error) {
|
||||
console.error('Failed to create user:', error);
|
||||
return json({ error: 'Failed to create user' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
42
src/routes/api/membership/[id]/+server.ts
Normal file
42
src/routes/api/membership/[id]/+server.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { fetchAPIEvent } from '$api/apiService';
|
||||
import { json, type RequestEvent } from '@sveltejs/kit';
|
||||
|
||||
export async function GET(event: RequestEvent) {
|
||||
try {
|
||||
const { params } = event;
|
||||
const userId = params.id;
|
||||
|
||||
const user = await fetchAPIEvent(event, `/membership/${userId}`);
|
||||
return json(user);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user:', error);
|
||||
return json({ error: 'Failed to fetch user' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(event: RequestEvent) {
|
||||
try {
|
||||
const { params } = event;
|
||||
const userId = params.id;
|
||||
const userData = await event.request.json();
|
||||
|
||||
const user = await fetchAPIEvent(event, `/membership/${userId}`, 'PUT', userData);
|
||||
return json(user);
|
||||
} catch (error) {
|
||||
console.error('Failed to update user:', error);
|
||||
return json({ error: 'Failed to update user' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(event: RequestEvent) {
|
||||
try {
|
||||
const { params } = event;
|
||||
const userId = params.id;
|
||||
|
||||
await fetchAPIEvent(event, `/membership/${userId}`, 'DELETE');
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Failed to delete user:', error);
|
||||
return json({ error: 'Failed to delete user' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ export const load = async (event: RequestEvent) => {
|
||||
|
||||
// Helper function to create a server action with validation and error handling
|
||||
const createServerAction = (
|
||||
action: (event: RequestEvent, id: number) => Promise<void>,
|
||||
action: (event: RequestEvent, id: string) => Promise<void>,
|
||||
{ success, failure }: { success: string; failure: string }
|
||||
) => {
|
||||
return async (event: RequestEvent) => {
|
||||
@@ -22,13 +22,8 @@ const createServerAction = (
|
||||
return fail(400, { message: 'Invalid server ID provided.' });
|
||||
}
|
||||
|
||||
const serverId = Number(id);
|
||||
if (isNaN(serverId)) {
|
||||
return fail(400, { message: 'Server ID must be a number.' });
|
||||
}
|
||||
|
||||
try {
|
||||
await action(event, serverId);
|
||||
await action(event, id);
|
||||
return { success: true, message: success };
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Unknown error';
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
|
||||
<h1 class="text-2xl font-bold">ACC Server Manager</h1>
|
||||
<div class="flex items-center space-x-4">
|
||||
{#if false && hasPermission($user, 'membership.view')}
|
||||
{#if hasPermission($user, 'membership.view')}
|
||||
<a href="/dashboard/membership" class="flex items-center text-gray-300 hover:text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,18 +1,116 @@
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { fail, redirect, type Actions } from '@sveltejs/kit';
|
||||
import { membershipService } from '$api/membershipService';
|
||||
|
||||
export const load = async ({ locals, fetch }) => {
|
||||
if (!locals.user) {
|
||||
export const load = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
throw redirect(303, '/login');
|
||||
}
|
||||
|
||||
const response = await fetch('/api/users');
|
||||
if (!response.ok) {
|
||||
return { users: [] };
|
||||
}
|
||||
// Get filter parameters from URL
|
||||
const page = parseInt(event.url.searchParams.get('page') || '1');
|
||||
const pageSize = parseInt(event.url.searchParams.get('page_size') || '10');
|
||||
const username = event.url.searchParams.get('username') || '';
|
||||
const roleName = event.url.searchParams.get('role_name') || '';
|
||||
const sortBy = event.url.searchParams.get('sort_by') || 'username';
|
||||
const sortDesc = event.url.searchParams.get('sort_desc') === 'true';
|
||||
|
||||
const users = await response.json();
|
||||
const filter = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
username: username || undefined,
|
||||
role_name: roleName || undefined,
|
||||
sort_by: sortBy,
|
||||
sort_desc: sortDesc
|
||||
};
|
||||
|
||||
try {
|
||||
const [users, roles] = await Promise.all([
|
||||
membershipService.getUsers(event, filter),
|
||||
membershipService.getRoles(event)
|
||||
]);
|
||||
|
||||
console.log(users);
|
||||
// Simple client-side pagination
|
||||
const startIndex = (page - 1) * pageSize;
|
||||
const endIndex = startIndex + pageSize;
|
||||
const paginatedUsers = users.slice(startIndex, endIndex);
|
||||
const totalPages = Math.ceil(users.length / pageSize);
|
||||
|
||||
return {
|
||||
users
|
||||
users: paginatedUsers,
|
||||
roles,
|
||||
pagination: {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
total: users.length,
|
||||
total_pages: totalPages
|
||||
},
|
||||
filter: {
|
||||
username,
|
||||
role_name: roleName,
|
||||
sort_by: sortBy,
|
||||
sort_desc: sortDesc
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to load users or roles:', error);
|
||||
return {
|
||||
users: [],
|
||||
roles: [],
|
||||
pagination: {
|
||||
page: 1,
|
||||
page_size: 10,
|
||||
total: 0,
|
||||
total_pages: 0
|
||||
},
|
||||
filter: {
|
||||
username: '',
|
||||
role_name: '',
|
||||
sort_by: 'username',
|
||||
sort_desc: false
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const actions = {
|
||||
create: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const username = formData.get('username');
|
||||
const password = formData.get('password');
|
||||
const role = formData.get('role');
|
||||
|
||||
if (!username || !password || !role) {
|
||||
return fail(400, { message: 'All fields are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await membershipService.createUser(event, {
|
||||
username: username.toString(),
|
||||
password: password.toString(),
|
||||
role: role.toString()
|
||||
});
|
||||
return { success: true, message: 'User created successfully' };
|
||||
} catch (error) {
|
||||
console.error('Failed to create user:', error);
|
||||
return fail(500, { message: 'Failed to create user' });
|
||||
}
|
||||
},
|
||||
|
||||
delete: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const id = formData.get('id');
|
||||
|
||||
if (!id) {
|
||||
return fail(400, { message: 'User ID is required' });
|
||||
}
|
||||
|
||||
try {
|
||||
await membershipService.deleteUser(event, id.toString());
|
||||
return { success: true, message: 'User deleted successfully' };
|
||||
} catch (error) {
|
||||
console.error('Failed to delete user:', error);
|
||||
return fail(500, { message: 'Failed to delete user' });
|
||||
}
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
||||
@@ -1,46 +1,379 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import Pagination from '$components/Pagination.svelte';
|
||||
import Toast from '$components/Toast.svelte';
|
||||
import { user, hasPermission } from '$stores/user';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
const { data, form }: { data: PageData; form?: any } = $props();
|
||||
|
||||
$: users = data.users || [];
|
||||
let users = $derived(data.users || []);
|
||||
let roles = $derived(data.roles || []);
|
||||
let pagination = $derived(data.pagination);
|
||||
let filter = $derived(data.filter);
|
||||
|
||||
let usernameFilter = $state('');
|
||||
let roleNameFilter = $state('');
|
||||
let sortBy = $state('username');
|
||||
let sortDesc = $state(false);
|
||||
let showCreateModal = $state(false);
|
||||
let showDeleteModal = $state(false);
|
||||
let selectedUser: any = $state(null);
|
||||
let createForm = $state({
|
||||
username: '',
|
||||
password: '',
|
||||
role: ''
|
||||
});
|
||||
|
||||
// Initialize filters from data
|
||||
$effect(() => {
|
||||
usernameFilter = filter.username || '';
|
||||
roleNameFilter = filter.role_name || '';
|
||||
sortBy = filter.sort_by || 'username';
|
||||
sortDesc = filter.sort_desc || false;
|
||||
});
|
||||
|
||||
function applyFilters() {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
// Set filter parameters
|
||||
if (usernameFilter) params.set('username', usernameFilter);
|
||||
if (roleNameFilter) params.set('role_name', roleNameFilter);
|
||||
params.set('sort_by', sortBy);
|
||||
params.set('sort_desc', sortDesc.toString());
|
||||
params.set('page', '1'); // Reset to first page
|
||||
|
||||
goto(`?${params.toString()}`, { invalidateAll: true });
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
usernameFilter = '';
|
||||
roleNameFilter = '';
|
||||
sortBy = 'username';
|
||||
sortDesc = false;
|
||||
goto('/dashboard/membership', { invalidateAll: true });
|
||||
}
|
||||
|
||||
function handlePageChange(newPage: number) {
|
||||
const params = new URLSearchParams($page.url.searchParams);
|
||||
params.set('page', newPage.toString());
|
||||
goto(`?${params.toString()}`, { invalidateAll: true });
|
||||
}
|
||||
|
||||
function handleSort(field: string) {
|
||||
if (sortBy === field) {
|
||||
sortDesc = !sortDesc;
|
||||
} else {
|
||||
sortBy = field;
|
||||
sortDesc = false;
|
||||
}
|
||||
applyFilters();
|
||||
}
|
||||
|
||||
function getSortIcon(field: string) {
|
||||
if (sortBy !== field) return '↕️';
|
||||
return sortDesc ? '↓' : '↑';
|
||||
}
|
||||
|
||||
function openCreateModal() {
|
||||
showCreateModal = true;
|
||||
createForm = { username: '', password: '', role: '' };
|
||||
}
|
||||
|
||||
function closeCreateModal() {
|
||||
showCreateModal = false;
|
||||
}
|
||||
|
||||
function openDeleteModal(userItem: any) {
|
||||
selectedUser = userItem;
|
||||
showDeleteModal = true;
|
||||
}
|
||||
|
||||
function closeDeleteModal() {
|
||||
selectedUser = null;
|
||||
showDeleteModal = false;
|
||||
}
|
||||
|
||||
// Close modals after successful form submission
|
||||
$effect(() => {
|
||||
if (form?.success) {
|
||||
showCreateModal = false;
|
||||
showDeleteModal = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto p-4">
|
||||
<h1 class="mb-4 text-2xl font-bold">User Management</h1>
|
||||
{#if form?.message}
|
||||
<Toast message={form.message} type={form.success ? 'success' : 'error'} />
|
||||
{/if}
|
||||
|
||||
{#if hasPermission($user, 'membership.create')}
|
||||
<div class="mb-4 flex justify-end">
|
||||
<button class="btn btn-primary">Create User</button>
|
||||
<div class="min-h-screen bg-gray-900 text-white">
|
||||
<header class="bg-gray-800 shadow-md">
|
||||
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6 lg:px-8">
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="/dashboard" class="text-gray-300 hover:text-white" aria-label="Back to dashboard">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold">User Management</h1>
|
||||
</div>
|
||||
{#if hasPermission($user, 'membership.create')}
|
||||
<button
|
||||
onclick={openCreateModal}
|
||||
class="rounded-md bg-green-600 px-4 py-2 text-sm font-medium hover:bg-green-700"
|
||||
>
|
||||
Create User
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table w-full">
|
||||
<thead>
|
||||
<main class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<!-- Filters -->
|
||||
<div class="mb-6 rounded-lg border border-gray-700 bg-gray-800 p-4">
|
||||
<h2 class="mb-3 text-lg font-semibold">Filters</h2>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-gray-300">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
bind:value={usernameFilter}
|
||||
placeholder="Search by username..."
|
||||
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="role" class="block text-sm font-medium text-gray-300">Role</label>
|
||||
<input
|
||||
id="role"
|
||||
type="text"
|
||||
bind:value={roleNameFilter}
|
||||
placeholder="Filter by role..."
|
||||
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white placeholder-gray-400 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex space-x-2">
|
||||
<button
|
||||
onclick={applyFilters}
|
||||
class="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium hover:bg-blue-700"
|
||||
>
|
||||
Apply Filters
|
||||
</button>
|
||||
<button
|
||||
onclick={resetFilters}
|
||||
class="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium hover:bg-gray-700"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Results Summary -->
|
||||
<div class="mb-4 text-sm text-gray-400">
|
||||
Showing {users.length} of {pagination.total} users
|
||||
{#if pagination.total_pages > 1}
|
||||
(Page {pagination.page} of {pagination.total_pages})
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Users Table -->
|
||||
<div class="overflow-hidden rounded-lg border border-gray-700 bg-gray-800">
|
||||
<table class="min-w-full divide-y divide-gray-700">
|
||||
<thead class="bg-gray-900">
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Role</th>
|
||||
<th class="px-6 py-3 text-left">
|
||||
<button
|
||||
class="flex items-center space-x-1 text-xs font-medium tracking-wider text-gray-400 uppercase hover:text-white"
|
||||
onclick={() => handleSort('username')}
|
||||
>
|
||||
<span>Username</span>
|
||||
<span>{getSortIcon('username')}</span>
|
||||
</button>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left">
|
||||
<button
|
||||
class="flex items-center space-x-1 text-xs font-medium tracking-wider text-gray-400 uppercase hover:text-white"
|
||||
onclick={() => handleSort('role')}
|
||||
>
|
||||
<span>Role</span>
|
||||
<span>{getSortIcon('role')}</span>
|
||||
</button>
|
||||
</th>
|
||||
{#if hasPermission($user, 'membership.edit')}
|
||||
<th>Actions</th>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium tracking-wider text-gray-400 uppercase"
|
||||
>
|
||||
Actions
|
||||
</th>
|
||||
{/if}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each users as user (user.id)}
|
||||
<tr>
|
||||
<td>{user.username}</td>
|
||||
<td>{user.role.name}</td>
|
||||
{#if hasPermission(user, 'membership.edit')}
|
||||
<td>
|
||||
<button class="btn btn-sm">Edit</button>
|
||||
<button class="btn btn-sm btn-error">Delete</button>
|
||||
<tbody class="divide-y divide-gray-700 bg-gray-800">
|
||||
{#each users as userItem (userItem.id)}
|
||||
<tr class="hover:bg-gray-700">
|
||||
<td class="px-6 py-4 text-sm font-medium whitespace-nowrap text-white">
|
||||
{userItem.username}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm whitespace-nowrap">
|
||||
<span
|
||||
class="inline-flex rounded-full bg-blue-900 px-2 py-1 text-xs font-semibold text-blue-300"
|
||||
>
|
||||
{userItem.role.name}
|
||||
</span>
|
||||
</td>
|
||||
{#if hasPermission($user, 'membership.edit')}
|
||||
<td class="px-6 py-4 text-sm font-medium whitespace-nowrap">
|
||||
<div class="flex space-x-2">
|
||||
<button class="text-blue-400 hover:text-blue-300"> Edit </button>
|
||||
{#if hasPermission($user, 'membership.delete')}
|
||||
<button
|
||||
onclick={() => openDeleteModal(userItem)}
|
||||
class="text-red-400 hover:text-red-300"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
{/if}
|
||||
</tr>
|
||||
{:else}
|
||||
<tr>
|
||||
<td colspan="3" class="px-6 py-4 text-center text-sm text-gray-400">
|
||||
No users found
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{#if pagination.total_pages > 1}
|
||||
<div class="mt-6 flex justify-center">
|
||||
<Pagination
|
||||
currentPage={pagination.page}
|
||||
totalPages={pagination.total_pages}
|
||||
onPageChange={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Create User Modal -->
|
||||
{#if showCreateModal}
|
||||
<div class="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
|
||||
<div class="w-full max-w-md rounded-lg bg-gray-800 p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-white">Create New User</h3>
|
||||
<form method="POST" action="?/create">
|
||||
<div class="mb-4">
|
||||
<label for="create-username" class="block text-sm font-medium text-gray-300"
|
||||
>Username</label
|
||||
>
|
||||
<input
|
||||
id="create-username"
|
||||
name="username"
|
||||
type="text"
|
||||
bind:value={createForm.username}
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="create-password" class="block text-sm font-medium text-gray-300"
|
||||
>Password</label
|
||||
>
|
||||
<input
|
||||
id="create-password"
|
||||
name="password"
|
||||
type="password"
|
||||
bind:value={createForm.password}
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label for="create-role" class="block text-sm font-medium text-gray-300">Role</label>
|
||||
<select
|
||||
id="create-role"
|
||||
name="role"
|
||||
bind:value={createForm.role}
|
||||
required
|
||||
class="mt-1 block w-full rounded-md border border-gray-600 bg-gray-700 px-3 py-2 text-white focus:border-blue-500 focus:ring-1 focus:ring-blue-500 focus:outline-none"
|
||||
>
|
||||
<option value="">Select a role...</option>
|
||||
{#each roles as role}
|
||||
<option value={role.name}>{role.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeCreateModal}
|
||||
class="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-green-600 px-4 py-2 text-sm font-medium hover:bg-green-700"
|
||||
>
|
||||
Create User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
{#if showDeleteModal && selectedUser}
|
||||
<div class="bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black">
|
||||
<div class="w-full max-w-md rounded-lg bg-gray-800 p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-white">Delete User</h3>
|
||||
<p class="mb-6 text-gray-300">
|
||||
Are you sure you want to delete the user "{selectedUser.username}"? This action cannot be
|
||||
undone.
|
||||
</p>
|
||||
<form method="POST" action="?/delete">
|
||||
<input type="hidden" name="id" value={selectedUser.id} />
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={closeDeleteModal}
|
||||
class="rounded-md bg-gray-600 px-4 py-2 text-sm font-medium hover:bg-gray-700"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium hover:bg-red-700"
|
||||
>
|
||||
Delete User
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:head>
|
||||
<title>User Management - ACC Server Manager</title>
|
||||
</svelte:head>
|
||||
|
||||
Reference in New Issue
Block a user