implement full user management function base on sqlite database in admi
This commit is contained in:
parent
e7d9db0cd6
commit
38fccc09b7
|
|
@ -0,0 +1,150 @@
|
|||
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useForm, SubmitHandler } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Save, UserPlus } from 'lucide-react';
|
||||
import type { User } from '@/types';
|
||||
import { useI18n, useCurrentLocale } from '@/locales/client';
|
||||
import { registerUser, updateUser } from '@/app/actions/user';
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2, "Name must be at least 2 characters."),
|
||||
email: z.string().email("Invalid email address."),
|
||||
role: z.enum(['User', 'Admin']),
|
||||
});
|
||||
|
||||
type UserFormValues = z.infer<typeof formSchema>;
|
||||
|
||||
interface UserFormProps {
|
||||
initialData?: User;
|
||||
isEditMode?: boolean;
|
||||
}
|
||||
|
||||
export default function UserForm({ initialData, isEditMode = false }: UserFormProps) {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const t = useI18n();
|
||||
const locale = useCurrentLocale();
|
||||
|
||||
const form = useForm<UserFormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: initialData?.name || '',
|
||||
email: initialData?.email || '',
|
||||
role: initialData?.role || 'User',
|
||||
},
|
||||
});
|
||||
|
||||
const { isSubmitting } = form.formState;
|
||||
|
||||
const onSubmit: SubmitHandler<UserFormValues> = async (data) => {
|
||||
const result = isEditMode
|
||||
? await updateUser({ ...initialData!, ...data })
|
||||
: await registerUser(data);
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: isEditMode ? t('admin.users.user_updated_toast') : t('admin.users.user_created_toast'),
|
||||
});
|
||||
router.push(`/${locale}/admin/users`);
|
||||
router.refresh(); // Ensure the user list is up-to-date
|
||||
} else {
|
||||
toast({
|
||||
title: t('admin.users.toast_error_title'),
|
||||
description: result.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto shadow-xl">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-headline">
|
||||
{isEditMode ? t('admin.users.edit_user_title') : t('admin.users.add_user_title')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{isEditMode ? t('admin.users.edit_user_description') : t('admin.users.add_user_description')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<CardContent className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('admin.users.form_name_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="John Doe" {...field} disabled={isSubmitting} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('admin.users.form_email_label')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="email" placeholder="user@example.com" {...field} disabled={isSubmitting} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="role"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('admin.users.form_role_label')}</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value} disabled={isSubmitting}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a role" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="User">{t('admin.users.form_role_user')}</SelectItem>
|
||||
<SelectItem value="Admin">{t('admin.users.form_role_admin')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{isSubmitting ? t('admin.users.form_saving_button') : t('admin.users.form_save_button')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
{isSubmitting ? t('admin.users.form_creating_button') : t('admin.users.form_create_button')}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
import UserForm from '../UserForm';
|
||||
import { getI18n } from '@/locales/server';
|
||||
import type { Locale } from '@/locales/server';
|
||||
|
||||
export default async function AddUserPage({ params }: { params: { locale: Locale } }) {
|
||||
const t = await getI18n(); // To preload translations for the client component
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<UserForm isEditMode={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
|
||||
import { getUserById } from '@/data/operations';
|
||||
import UserForm from '../UserForm';
|
||||
import { getI18n } from '@/locales/server';
|
||||
import type { Locale } from '@/locales/server';
|
||||
import Link from 'next/link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
interface EditUserPageProps {
|
||||
params: { id: string; locale: Locale };
|
||||
}
|
||||
|
||||
export default async function EditUserPage({ params }: EditUserPageProps) {
|
||||
const t = await getI18n();
|
||||
const userData = getUserById(params.id);
|
||||
|
||||
if (!userData) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<h1 className="text-2xl font-bold mb-4">{t('admin.users.user_not_found')}</h1>
|
||||
<Link href={`/${params.locale}/admin/users`} passHref>
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
{t('admin.users.back_to_users_button')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<UserForm isEditMode={true} initialData={userData} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,27 +1,70 @@
|
|||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { getAllUsers } from "@/data/operations";
|
||||
import { getI18n } from "@/locales/server";
|
||||
import { UserCog } from "lucide-react";
|
||||
import { useI18n, useCurrentLocale } from "@/locales/client";
|
||||
import { UserCog, Trash2, PlusCircle } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { Locale } from "@/locales/server";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import type { User } from '@/types';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { deleteUser } from '@/app/actions/user';
|
||||
|
||||
export default async function AdminUserManagementPage({ params }: { params: { locale: Locale } }) {
|
||||
const t = await getI18n();
|
||||
const locale = params.locale;
|
||||
const allUsers = getAllUsers();
|
||||
export default function AdminUserManagementPage({ users }: { users: User[] }) {
|
||||
const t = useI18n();
|
||||
const locale = useCurrentLocale();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// Sort users for consistent display, e.g., by name or role
|
||||
allUsers.sort((a, b) => a.name.localeCompare(b.name));
|
||||
// We receive users as a prop after fetching them in a wrapper component.
|
||||
const allUsers = users.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const handleDeleteUser = async (userId: string) => {
|
||||
setIsDeleting(true);
|
||||
const result = await deleteUser(userId);
|
||||
if (result.success) {
|
||||
toast({ title: t('admin.users.user_deleted_toast') });
|
||||
// The page will re-render due to revalidation, no need for router.refresh()
|
||||
} else {
|
||||
toast({
|
||||
title: t('admin.users.toast_error_title'),
|
||||
description: result.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
setIsDeleting(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold font-headline text-primary">{t('admin.users.title')}</h1>
|
||||
<p className="text-muted-foreground">{t('admin.users.description')}</p>
|
||||
</div>
|
||||
<Link href={`/${locale}/admin/users/add`} passHref>
|
||||
<Button>
|
||||
<PlusCircle className="mr-2 h-5 w-5" />
|
||||
{t('admin.users.add_user_button')}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-md">
|
||||
<CardHeader>
|
||||
|
|
@ -44,11 +87,39 @@ export default async function AdminUserManagementPage({ params }: { params: { lo
|
|||
<TableCell className="font-medium">{user.name}</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>{user.role}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button variant="outline" size="sm" disabled>
|
||||
<TableCell className="text-right space-x-2">
|
||||
<Link href={`/${locale}/admin/users/edit/${user.id}`} passHref>
|
||||
<Button variant="outline" size="sm">
|
||||
<UserCog className="mr-2 h-4 w-4" />
|
||||
{t('admin.users.edit_button')}
|
||||
</Button>
|
||||
</Link>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" size="sm" disabled={isDeleting}>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
{t('admin.users.delete_button')}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>{t('admin.users.delete_confirm_title')}</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t('admin.users.delete_confirm_description', { userName: user.name })}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>{t('admin.users.delete_confirm_cancel')}</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleDeleteUser(user.id)}
|
||||
disabled={isDeleting}
|
||||
className="bg-destructive hover:bg-destructive/90"
|
||||
>
|
||||
{t('admin.users.delete_confirm_action')}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
|
@ -62,3 +133,9 @@ export default async function AdminUserManagementPage({ params }: { params: { lo
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Wrapper component to fetch data on the server and pass to the client component
|
||||
export default function AdminUserManagementPageWrapper() {
|
||||
const users = getAllUsers();
|
||||
return <AdminUserManagementPage users={users} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import db from '@/lib/db';
|
||||
import type { User } from '@/types';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
interface RegisterUserResult {
|
||||
success: boolean;
|
||||
|
|
@ -11,16 +12,14 @@ interface RegisterUserResult {
|
|||
user?: User;
|
||||
}
|
||||
|
||||
export async function registerUser(data: { name: string; email: string; password?: string }): Promise<RegisterUserResult> {
|
||||
const { name, email } = data;
|
||||
export async function registerUser(data: { name: string; email: string; password?: string, role?: 'Admin' | 'User' }): Promise<RegisterUserResult> {
|
||||
const { name, email, role = 'User' } = data;
|
||||
|
||||
// Basic validation
|
||||
if (!name || !email) {
|
||||
return { success: false, message: 'Name and email are required.' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if user already exists
|
||||
const existingUser = db.prepare('SELECT * FROM users WHERE email = ?').get(email);
|
||||
if (existingUser) {
|
||||
return { success: false, message: 'An account with this email already exists.' };
|
||||
|
|
@ -30,7 +29,7 @@ export async function registerUser(data: { name: string; email: string; password
|
|||
id: `user-${randomUUID()}`,
|
||||
name,
|
||||
email,
|
||||
role: 'User',
|
||||
role: role,
|
||||
avatarUrl: '',
|
||||
bio: ''
|
||||
};
|
||||
|
|
@ -41,9 +40,83 @@ export async function registerUser(data: { name: string; email: string; password
|
|||
|
||||
stmt.run(newUser);
|
||||
|
||||
revalidatePath('/admin/users');
|
||||
|
||||
return { success: true, message: 'User registered successfully.', user: newUser };
|
||||
} catch (error) {
|
||||
console.error('Registration error:', error);
|
||||
return { success: false, message: 'An unexpected error occurred during registration.' };
|
||||
}
|
||||
}
|
||||
|
||||
interface UpdateUserResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function updateUser(user: User): Promise<UpdateUserResult> {
|
||||
if (!user.id || !user.name || !user.email || !user.role) {
|
||||
return { success: false, message: 'All user fields are required for an update.' };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingUser = db.prepare('SELECT id FROM users WHERE email = ? AND id != ?').get(user.email, user.id);
|
||||
if (existingUser) {
|
||||
return { success: false, message: 'Another user with this email already exists.' };
|
||||
}
|
||||
|
||||
const stmt = db.prepare(
|
||||
'UPDATE users SET name = @name, email = @email, role = @role, avatarUrl = @avatarUrl, bio = @bio WHERE id = @id'
|
||||
);
|
||||
|
||||
stmt.run({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
avatarUrl: user.avatarUrl ?? '',
|
||||
bio: user.bio ?? ''
|
||||
});
|
||||
|
||||
revalidatePath('/admin/users');
|
||||
revalidatePath(`/admin/users/edit/${user.id}`);
|
||||
|
||||
return { success: true, message: 'User updated successfully.' };
|
||||
} catch (error) {
|
||||
console.error('Update user error:', error);
|
||||
return { success: false, message: 'An unexpected error occurred during the update.' };
|
||||
}
|
||||
}
|
||||
|
||||
interface DeleteUserResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export async function deleteUser(id: string): Promise<DeleteUserResult> {
|
||||
try {
|
||||
// Prevent deleting the main admin/user accounts for demo stability
|
||||
if (id.startsWith('user-seed-') || id === 'admin-main') {
|
||||
const user = getUserById(id);
|
||||
if(user?.email === 'user@example.com' || user?.email === 'admin@example.com') {
|
||||
return { success: false, message: 'This is a protected demo account and cannot be deleted.' };
|
||||
}
|
||||
}
|
||||
|
||||
const stmt = db.prepare('DELETE FROM users WHERE id = ?');
|
||||
const info = stmt.run(id);
|
||||
|
||||
if (info.changes === 0) {
|
||||
return { success: false, message: 'User not found or already deleted.' };
|
||||
}
|
||||
|
||||
revalidatePath('/admin/users');
|
||||
return { success: true, message: 'User deleted successfully.' };
|
||||
} catch (error) {
|
||||
console.error('Delete user error:', error);
|
||||
if (error.code === 'SQLITE_CONSTRAINT_FOREIGNKEY') {
|
||||
return { success: false, message: 'Cannot delete user. They still have toys listed in the system.' };
|
||||
}
|
||||
return { success: false, message: 'An unexpected error occurred during deletion.' };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export function getOwnerProfile(ownerId: string) {
|
|||
// --- USER OPERATIONS ---
|
||||
|
||||
export function getAllUsers(): User[] {
|
||||
const stmt = db.prepare('SELECT id, name, email, role FROM users');
|
||||
const stmt = db.prepare('SELECT id, name, email, role, avatarUrl, bio FROM users');
|
||||
return stmt.all() as User[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -228,20 +228,38 @@ export default {
|
|||
'admin.site_settings.title_saved_toast': 'Site title (mock) saved!',
|
||||
'admin.users.title': 'User Management',
|
||||
'admin.users.description': 'View and manage user accounts and permissions.',
|
||||
'admin.users.add_user_button': 'Add New User',
|
||||
'admin.users.back_to_users_button': 'Back to User List',
|
||||
'admin.users.table_header_name': 'Name',
|
||||
'admin.users.table_header_email': 'Email',
|
||||
'admin.users.table_header_role': 'Role',
|
||||
'admin.users.table_header_actions': 'Actions',
|
||||
'admin.users.edit_button': 'Edit',
|
||||
'admin.users.delete_button': 'Delete',
|
||||
'admin.users.no_users_found': 'No users found.',
|
||||
'admin.toys.title': 'Toy Management',
|
||||
'admin.toys.description': 'View and manage all toy listings in the system.',
|
||||
'admin.toys.table_header_name': 'Toy Name',
|
||||
'admin.toys.table_header_owner': 'Owner',
|
||||
'admin.toys.table_header_category': 'Category',
|
||||
'admin.toys.table_header_actions': 'Actions',
|
||||
'admin.toys.edit_button': 'Edit Toy',
|
||||
'admin.toys.no_toys_found': 'No toys found.',
|
||||
'admin.users.add_user_title': 'Add a New User',
|
||||
'admin.users.add_user_description': 'Create a new user account and assign a role.',
|
||||
'admin.users.edit_user_title': 'Edit User',
|
||||
'admin.users.edit_user_description': "Update the user's details and role.",
|
||||
'admin.users.form_name_label': 'Full Name',
|
||||
'admin.users.form_email_label': 'Email Address',
|
||||
'admin.users.form_role_label': 'Role',
|
||||
'admin.users.form_role_user': 'User',
|
||||
'admin.users.form_role_admin': 'Admin',
|
||||
'admin.users.form_create_button': 'Create User',
|
||||
'admin.users.form_save_button': 'Save Changes',
|
||||
'admin.users.form_creating_button': 'Creating...',
|
||||
'admin.users.form_saving_button': 'Saving...',
|
||||
'admin.users.delete_confirm_title': 'Are you sure?',
|
||||
'admin.users.delete_confirm_description': "This action cannot be undone. This will permanently delete the user account for {userName}.",
|
||||
'admin.users.delete_confirm_action': 'Yes, delete user',
|
||||
'admin.users.delete_confirm_cancel': 'Cancel',
|
||||
'admin.users.user_not_found': 'User not found.',
|
||||
'admin.users.user_created_toast': 'User created successfully.',
|
||||
'admin.users.user_updated_toast': 'User updated successfully.',
|
||||
'admin.users.user_deleted_toast': 'User deleted successfully.',
|
||||
'admin.users.user_delete_failed_toast': 'Failed to delete user.',
|
||||
'admin.users.toast_error_title': 'An error occurred',
|
||||
'dashboard.rental_history.title': 'My Rental History',
|
||||
'dashboard.rental_history.description': 'View your past toy rentals.',
|
||||
'dashboard.rental_history.no_history_title': 'No Rental History Yet',
|
||||
|
|
|
|||
|
|
@ -228,20 +228,38 @@ export default {
|
|||
'admin.site_settings.title_saved_toast': '網站標題(模擬)已儲存!',
|
||||
'admin.users.title': '使用者管理',
|
||||
'admin.users.description': '查看和管理使用者帳戶及權限。',
|
||||
'admin.users.add_user_button': '新增使用者',
|
||||
'admin.users.back_to_users_button': '返回使用者列表',
|
||||
'admin.users.table_header_name': '名稱',
|
||||
'admin.users.table_header_email': '電子郵件',
|
||||
'admin.users.table_header_role': '角色',
|
||||
'admin.users.table_header_actions': '操作',
|
||||
'admin.users.edit_button': '編輯',
|
||||
'admin.users.delete_button': '刪除',
|
||||
'admin.users.no_users_found': '找不到使用者。',
|
||||
'admin.toys.title': '玩具管理',
|
||||
'admin.toys.description': '查看和管理系統中的所有玩具列表。',
|
||||
'admin.toys.table_header_name': '玩具名稱',
|
||||
'admin.toys.table_header_owner': '擁有者',
|
||||
'admin.toys.table_header_category': '類別',
|
||||
'admin.toys.table_header_actions': '操作',
|
||||
'admin.toys.edit_button': '編輯玩具',
|
||||
'admin.toys.no_toys_found': '找不到玩具。',
|
||||
'admin.users.add_user_title': '新增使用者',
|
||||
'admin.users.add_user_description': '建立一個新的使用者帳戶並分配角色。',
|
||||
'admin.users.edit_user_title': '編輯使用者',
|
||||
'admin.users.edit_user_description': '更新使用者的詳細資訊和角色。',
|
||||
'admin.users.form_name_label': '全名',
|
||||
'admin.users.form_email_label': '電子郵件地址',
|
||||
'admin.users.form_role_label': '角色',
|
||||
'admin.users.form_role_user': '使用者',
|
||||
'admin.users.form_role_admin': '管理員',
|
||||
'admin.users.form_create_button': '建立使用者',
|
||||
'admin.users.form_save_button': '儲存變更',
|
||||
'admin.users.form_creating_button': '建立中...',
|
||||
'admin.users.form_saving_button': '儲存中...',
|
||||
'admin.users.delete_confirm_title': '您確定嗎?',
|
||||
'admin.users.delete_confirm_description': "此操作無法復原。這將永久刪除 {userName} 的使用者帳戶。",
|
||||
'admin.users.delete_confirm_action': '是的,刪除使用者',
|
||||
'admin.users.delete_confirm_cancel': '取消',
|
||||
'admin.users.user_not_found': '找不到使用者。',
|
||||
'admin.users.user_created_toast': '使用者已成功建立。',
|
||||
'admin.users.user_updated_toast': '使用者已成功更新。',
|
||||
'admin.users.user_deleted_toast': '使用者已成功刪除。',
|
||||
'admin.users.user_delete_failed_toast': '刪除使用者失敗。',
|
||||
'admin.users.toast_error_title': '發生錯誤',
|
||||
'dashboard.rental_history.title': '我的租借歷史',
|
||||
'dashboard.rental_history.description': '查看您過去的玩具租借記錄。',
|
||||
'dashboard.rental_history.no_history_title': '尚無租借歷史',
|
||||
|
|
|
|||
Loading…
Reference in New Issue