1
0
Code Issues Pull Requests Actions Packages Projects Releases Wiki Activity Security Code Quality

Add user page

This commit is contained in:
2025-07-06 21:28:45 +07:00
parent 76ca36ca1b
commit ae1a45fe57
24 changed files with 2661 additions and 232 deletions

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, initDB } from '@/database/database';
import { db, initDB, resetDB } from '@/database/database';
export async function POST(req: NextRequest) {
await initDB();

View File

@@ -0,0 +1,18 @@
import { NextResponse } from "next/server";
import db from "@/database/db.json";
// GET /api/permissions - Get all available permissions
export async function GET() {
try {
// Get all active permissions
const permissions = db.permissions?.filter(p => p.isActive) || [];
return NextResponse.json(permissions);
} catch (error) {
console.error("Error fetching permissions:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,234 @@
import { NextRequest, NextResponse } from "next/server";
import db from "@/database/db.json";
import { writeFileSync } from "fs";
import { join } from "path";
import { UserPermission, User, Permission } from "@/database/database.schema";
// Helper function to write to db.json
function writeDB() {
const dbPath = join(process.cwd(), "src/database/db.json");
writeFileSync(dbPath, JSON.stringify(db, null, 2));
}
// GET /api/user/[id]/permissions - Get all permissions for a user
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const userId = parseInt(params.id);
if (isNaN(userId)) {
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });
}
// Check if user exists
const user = db.users?.find((u: User) => u.id === userId);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// Get user permissions with permission details
const userPermissions = db.userPermissions?.filter((up: UserPermission) => up.userId === userId) || [];
const userPermissionsWithDetails = userPermissions.map((up: UserPermission) => {
const permission = db.permissions?.find((p: Permission) => p.id === up.permissionId);
return {
...up,
permission
};
});
return NextResponse.json(userPermissionsWithDetails);
} catch (error) {
console.error("Error fetching user permissions:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
// POST /api/user/[id]/permissions - Add permissions to a user
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const userId = parseInt(params.id);
if (isNaN(userId)) {
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });
}
// Check if user exists
const user = db.users?.find((u: User) => u.id === userId);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
const { permissionIds } = await request.json();
if (!Array.isArray(permissionIds)) {
return NextResponse.json({ error: "Permission IDs must be an array" }, { status: 400 });
}
// Validate permission IDs
const validPermissionIds = permissionIds.filter((id: number) =>
db.permissions?.find((p: Permission) => p.id === id && p.isActive)
);
if (validPermissionIds.length !== permissionIds.length) {
return NextResponse.json({ error: "Some permission IDs are invalid" }, { status: 400 });
}
// Remove existing permissions for this user
if (db.userPermissions) {
db.userPermissions = db.userPermissions.filter((up: UserPermission) => up.userId !== userId);
}
// Add new permissions
const currentUserPermissions = db.userPermissions || [];
const maxId = currentUserPermissions.length > 0 ? Math.max(...currentUserPermissions.map((up: UserPermission) => up.id)) : 0;
const newUserPermissions: UserPermission[] = validPermissionIds.map((permissionId: number, index: number) => ({
id: maxId + index + 1,
userId,
permissionId
}));
if (db.userPermissions) {
db.userPermissions.push(...newUserPermissions);
} else {
db.userPermissions = newUserPermissions;
}
writeDB();
// Get updated permissions with details
const updatedPermissions = newUserPermissions.map((up: UserPermission) => {
const permission = db.permissions?.find((p: Permission) => p.id === up.permissionId);
return {
...up,
permission
};
});
return NextResponse.json(updatedPermissions);
} catch (error) {
console.error("Error updating user permissions:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
// PUT /api/user/[id]/permissions - Update permissions for a user
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const userId = parseInt(params.id);
if (isNaN(userId)) {
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });
}
// Check if user exists
const user = db.users?.find((u: User) => u.id === userId);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
const { permissionIds } = await request.json();
if (!Array.isArray(permissionIds)) {
return NextResponse.json({ error: "Permission IDs must be an array" }, { status: 400 });
}
// Validate permission IDs
const validPermissionIds = permissionIds.filter((id: number) =>
db.permissions?.find((p: Permission) => p.id === id && p.isActive)
);
if (validPermissionIds.length !== permissionIds.length) {
return NextResponse.json({ error: "Some permission IDs are invalid" }, { status: 400 });
}
// Remove existing permissions for this user
if (db.userPermissions) {
db.userPermissions = db.userPermissions.filter((up: UserPermission) => up.userId !== userId);
}
// Add new permissions
const currentUserPermissions = db.userPermissions || [];
const maxId = currentUserPermissions.length > 0 ? Math.max(...currentUserPermissions.map((up: UserPermission) => up.id)) : 0;
const newUserPermissions: UserPermission[] = validPermissionIds.map((permissionId: number, index: number) => ({
id: maxId + index + 1,
userId,
permissionId
}));
if (db.userPermissions) {
db.userPermissions.push(...newUserPermissions);
} else {
db.userPermissions = newUserPermissions;
}
writeDB();
// Get updated permissions with details
const updatedPermissions = newUserPermissions.map((up: UserPermission) => {
const permission = db.permissions?.find((p: Permission) => p.id === up.permissionId);
return {
...up,
permission
};
});
return NextResponse.json(updatedPermissions);
} catch (error) {
console.error("Error updating user permissions:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
// DELETE /api/user/[id]/permissions - Remove all permissions from a user
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const userId = parseInt(params.id);
if (isNaN(userId)) {
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });
}
// Check if user exists
const user = db.users?.find((u: User) => u.id === userId);
if (!user) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}
// Remove all permissions for this user
if (db.userPermissions) {
db.userPermissions = db.userPermissions.filter((up: UserPermission) => up.userId !== userId);
}
writeDB();
return NextResponse.json({ message: "All permissions removed successfully" });
} catch (error) {
console.error("Error removing user permissions:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,172 @@
import { NextResponse } from "next/server";
import db from "@/database/db.json";
import { writeFileSync } from "fs";
import { join } from "path";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const userId = parseInt(params.id);
if (isNaN(userId)) {
return NextResponse.json(
{ success: false, message: "Invalid user ID" },
{ status: 400 }
);
}
const user = db.users.find(u => u.id === userId && !u.isDeleted);
if (!user) {
return NextResponse.json(
{ success: false, message: "User not found" },
{ status: 404 }
);
}
// Return user without password
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password: _, ...userResponse } = user;
return NextResponse.json({
success: true,
data: userResponse,
});
} catch (error) {
console.error("Error fetching user:", error);
return NextResponse.json(
{ success: false, message: "Failed to fetch user" },
{ status: 500 }
);
}
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const userId = parseInt(params.id);
if (isNaN(userId)) {
return NextResponse.json(
{ success: false, message: "Invalid user ID" },
{ status: 400 }
);
}
const body = await request.json();
const { username, email, firstName, lastName, password } = body;
// Validate required fields
if (!username || !email || !firstName || !lastName) {
return NextResponse.json(
{ success: false, message: "Username, email, firstName, and lastName are required" },
{ status: 400 }
);
}
// Find user
const userIndex = db.users.findIndex(u => u.id === userId && !u.isDeleted);
if (userIndex === -1) {
return NextResponse.json(
{ success: false, message: "User not found" },
{ status: 404 }
);
}
// Check if username or email already exists (excluding current user)
const existingUser = db.users.find(
(user) =>
user.id !== userId &&
(user.username === username || user.email === email) &&
!user.isDeleted
);
if (existingUser) {
return NextResponse.json(
{ success: false, message: "Username or email already exists" },
{ status: 400 }
);
}
// Update user
const updatedUser = {
...db.users[userIndex],
username,
email,
firstName,
lastName,
...(password && { password }), // Only update password if provided
};
db.users[userIndex] = updatedUser;
// Write to file
const dbPath = join(process.cwd(), "src/database/db.json");
writeFileSync(dbPath, JSON.stringify(db, null, 2));
// Return success response (without password)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password: _, ...userResponse } = updatedUser;
return NextResponse.json({
success: true,
data: userResponse,
message: "User updated successfully",
});
} catch (error) {
console.error("Error updating user:", error);
return NextResponse.json(
{ success: false, message: "Failed to update user" },
{ status: 500 }
);
}
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const userId = parseInt(params.id);
if (isNaN(userId)) {
return NextResponse.json(
{ success: false, message: "Invalid user ID" },
{ status: 400 }
);
}
// Find user
const userIndex = db.users.findIndex(u => u.id === userId && !u.isDeleted);
if (userIndex === -1) {
return NextResponse.json(
{ success: false, message: "User not found" },
{ status: 404 }
);
}
// Soft delete user
db.users[userIndex].isDeleted = true;
// Write to file
const dbPath = join(process.cwd(), "src/database/db.json");
writeFileSync(dbPath, JSON.stringify(db, null, 2));
return NextResponse.json({
success: true,
message: "User deleted successfully",
});
} catch (error) {
console.error("Error deleting user:", error);
return NextResponse.json(
{ success: false, message: "Failed to delete user" },
{ status: 500 }
);
}
}

150
src/app/api/user/route.ts Normal file
View File

@@ -0,0 +1,150 @@
import { NextResponse } from "next/server";
import db from "@/database/db.json";
import { writeFileSync } from "fs";
import { join } from "path";
export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1", 10);
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10);
const search = (searchParams.get("search") || "").toLowerCase();
const sortBy = searchParams.get("sortBy") || "id";
const sortOrder = searchParams.get("sortOrder") || "asc";
const users = db.users || [];
// Filter users based on search query
let filtered = users.filter(user => !user.isDeleted);
if (search) {
filtered = filtered.filter(
(u) =>
`${u.firstName} ${u.lastName}`.toLowerCase().includes(search) ||
u.email.toLowerCase().includes(search) ||
u.username.toLowerCase().includes(search)
);
}
// Sort users
filtered.sort((a, b) => {
let aValue: string | number | boolean = a[sortBy as keyof typeof a];
let bValue: string | number | boolean = b[sortBy as keyof typeof b];
// Handle special cases
if (sortBy === "fullName") {
aValue = `${a.firstName} ${a.lastName}`;
bValue = `${b.firstName} ${b.lastName}`;
}
// Convert to comparable values
const aCompare = typeof aValue === "string" ? aValue.toLowerCase() : aValue;
const bCompare = typeof bValue === "string" ? bValue.toLowerCase() : bValue;
if (sortOrder === "desc") {
return aCompare > bCompare ? -1 : aCompare < bCompare ? 1 : 0;
} else {
return aCompare < bCompare ? -1 : aCompare > bCompare ? 1 : 0;
}
});
const total = filtered.length;
const totalPages = Math.ceil(total / pageSize);
const start = (page - 1) * pageSize;
const end = start + pageSize;
const data = filtered.slice(start, end);
const pagination = {
page,
pageSize,
total,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
};
return NextResponse.json({
success: true,
data,
pagination,
});
} catch (error) {
console.error("Error fetching users:", error);
return NextResponse.json({
success: false,
data: [],
pagination: {
page: 1,
pageSize: 10,
total: 0,
totalPages: 0,
hasNextPage: false,
hasPreviousPage: false,
},
message: "Failed to fetch users",
});
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
const { username, email, firstName, lastName, password } = body;
// Validate required fields
if (!username || !email || !firstName || !lastName || !password) {
return NextResponse.json(
{ success: false, message: "All fields are required" },
{ status: 400 }
);
}
// Check if username or email already exists
const existingUser = db.users.find(
(user) =>
(user.username === username || user.email === email) &&
!user.isDeleted
);
if (existingUser) {
return NextResponse.json(
{ success: false, message: "Username or email already exists" },
{ status: 400 }
);
}
// Generate new ID
const newId = Math.max(...db.users.map(u => u.id), 0) + 1;
// Create new user
const newUser = {
id: newId,
username,
email,
firstName,
lastName,
password, // Note: In production, this should be hashed
isDeleted: false,
};
// Add to database
db.users.push(newUser);
// Write to file
const dbPath = join(process.cwd(), "src/database/db.json");
writeFileSync(dbPath, JSON.stringify(db, null, 2));
// Return success response (without password)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { password: _, ...userResponse } = newUser;
return NextResponse.json(
{ success: true, data: userResponse, message: "User created successfully" },
{ status: 201 }
);
} catch (error) {
console.error("Error creating user:", error);
return NextResponse.json(
{ success: false, message: "Failed to create user" },
{ status: 500 }
);
}
}

View File

@@ -27,7 +27,7 @@ export default function LoginPage() {
const res = await axios.post("/api/auth", data);
if (res.data.success) {
// You can redirect or set session here
window.location.href = "/";
window.location.href = "/modules/user"; // Redirect to user page on successful login
} else {
setError(res.data.message || "Login failed");
}

View File

@@ -1,5 +1,6 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Toaster } from "@/components/ui/sonner";
import "./globals.css";
const geistSans = Geist({
@@ -28,6 +29,7 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Toaster />
</body>
</html>
);

View File

@@ -21,6 +21,7 @@ import {
type CustomerDependant,
} from "@/schemas/customer.schema";
import axios from "axios";
import { Header } from "@/components/common/header";
export default function CustomerEditPage() {
const params = useParams();
@@ -250,38 +251,46 @@ export default function CustomerEditPage() {
}
return (
<div className="container mx-auto p-6 max-w-6xl">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "Customer Management", href: "/modules/customer" },
{ title: "Edit Customer", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<Button
variant="outline"
onClick={() => router.push("/modules/customer")}
className="flex items-center space-x-2"
>
<ArrowLeft className="h-4 w-4" />
<span>Back to Customers</span>
</Button>
<h1 className="text-2xl font-bold">Edit Customer</h1>
</div>
<Button
variant="outline"
onClick={() => router.push("/modules/customer")}
type="submit"
form="customer-edit-form"
disabled={isSaving}
className="flex items-center space-x-2"
>
<ArrowLeft className="h-4 w-4" />
<span>Back to Customers</span>
{isSaving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<span>Saving...</span>
</>
) : (
<>
<Save className="h-4 w-4" />
<span>Save Changes</span>
</>
)}
</Button>
<h1 className="text-2xl font-bold">Edit Customer</h1>
</div>
<Button
type="submit"
form="customer-edit-form"
disabled={isSaving}
className="flex items-center space-x-2"
>
{isSaving ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
<span>Saving...</span>
</>
) : (
<>
<Save className="h-4 w-4" />
<span>Save Changes</span>
</>
)}
</Button>
</div>
<Form {...form}>
<form id="customer-edit-form" className="space-y-6" onSubmit={form.handleSubmit(onSubmit)}>
@@ -608,6 +617,7 @@ export default function CustomerEditPage() {
</Tabs>
</form>
</Form>
</div>
</div>
);
}

View File

@@ -21,6 +21,7 @@ import {
type CustomerDependant,
} from "@/schemas/customer.schema";
import axios from "axios";
import { Header } from "@/components/common/header";
export default function CustomerAddPage() {
const router = useRouter();
@@ -186,27 +187,35 @@ export default function CustomerAddPage() {
};
return (
<div className="container mx-auto p-6 max-w-6xl">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<Button
variant="outline"
onClick={() => router.push("/modules/customer")}
className="flex items-center space-x-2"
>
<ArrowLeft className="h-4 w-4" />
<span>Back to Customers</span>
</Button>
<h1 className="text-2xl font-bold">Add New Customer</h1>
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "Customer Management", href: "/modules/customer" },
{ title: "Add Customer", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-4">
<Button
variant="outline"
onClick={() => router.push("/modules/customer")}
className="flex items-center space-x-2"
>
<ArrowLeft className="h-4 w-4" />
<span>Back to Customers</span>
</Button>
<h1 className="text-2xl font-bold">Add New Customer</h1>
</div>
</div>
</div>
<Card>
<CardHeader>
<CardDescription>
Please fill in the customer information across the three tabs below.
</CardDescription>
</CardHeader>
<CardContent>
<Card>
<CardHeader>
<CardDescription>
Please fill in the customer information across the three tabs below.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<Tabs value={activeTab} className="w-full">
@@ -567,6 +576,7 @@ export default function CustomerAddPage() {
</Form>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -9,6 +9,7 @@ import { toast } from "sonner";
import axios from "axios";
import { ServerDataTable } from "@/components/ui/server-data-table";
import { Customer, createCustomerColumns } from "@/components/customers/customer-columns";
import { Header } from "@/components/common/header";
interface PaginationInfo {
page: number;
@@ -153,29 +154,36 @@ export default function CustomerPage() {
}
return (
<div className="container mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-2">
<Users className="h-8 w-8" />
<h1 className="text-3xl font-bold">Customer Management</h1>
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "Customer Management", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-2">
<Users className="h-8 w-8" />
<h1 className="text-3xl font-bold">Customer Management</h1>
</div>
<div className="flex items-center space-x-2">
<Button
onClick={handleRefresh}
variant="outline"
size="sm"
disabled={isRefreshing}
className="flex items-center space-x-2"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
<span>Refresh</span>
</Button>
<Button onClick={handleAddCustomer} className="flex items-center space-x-2">
<Plus className="h-4 w-4" />
<span>Add Customer</span>
</Button>
</div>
</div>
<div className="flex items-center space-x-2">
<Button
onClick={handleRefresh}
variant="outline"
size="sm"
disabled={isRefreshing}
className="flex items-center space-x-2"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
<span>Refresh</span>
</Button>
<Button onClick={handleAddCustomer} className="flex items-center space-x-2">
<Plus className="h-4 w-4" />
<span>Add Customer</span>
</Button>
</div>
</div>
<Card>
<CardHeader>
@@ -214,6 +222,7 @@ export default function CustomerPage() {
)}
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -1,11 +1,8 @@
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { Separator } from "@radix-ui/react-select";
export default function ModulesLayout({
children,
@@ -17,25 +14,6 @@ export default function ModulesLayout({
<SidebarProvider>
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator
className="mr-2 data-[orientation=vertical]:h-4"
/>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href="#">
Building Your Application
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
<BreadcrumbItem>
<BreadcrumbPage>Data Fetching</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</header>
{children}
</SidebarInset>
</SidebarProvider>

View File

@@ -2,14 +2,15 @@
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { ChevronDown, ChevronUp, Download, X, Settings } from "lucide-react"
import { ChevronDown, ChevronUp, Download, X, Settings, Mail, Send } from "lucide-react"
import { Header } from "@/components/common/header"
export default function MailTemplatePage() {
const [bodyExpanded, setBodyExpanded] = useState(true)
@@ -26,20 +27,43 @@ export default function MailTemplatePage() {
]
return (
<div className="min-h-screen bg-gray-50 p-4">
<div className="max-w-4xl mx-auto space-y-6">
{/* Header Buttons */}
<div className="flex justify-end gap-2">
<Button className="bg-slate-400 hover:bg-slate-500 text-white">
<Settings className="w-4 h-4 mr-2" />
registration
</Button>
<Button className="bg-orange-400 hover:bg-orange-500 text-white">Send a test email</Button>
<Button className="bg-gray-400 hover:bg-gray-500 text-white">Send E-mail</Button>
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "Mail Template", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-2">
<Mail className="h-8 w-8" />
<h1 className="text-3xl font-bold">Mail Template</h1>
</div>
<div className="flex items-center space-x-2">
<Button className="bg-slate-400 hover:bg-slate-500 text-white">
<Settings className="w-4 h-4 mr-2" />
Registration
</Button>
<Button className="bg-orange-400 hover:bg-orange-500 text-white">
<Send className="w-4 h-4 mr-2" />
Send Test Email
</Button>
<Button className="bg-gray-400 hover:bg-gray-500 text-white">
<Mail className="w-4 h-4 mr-2" />
Send E-mail
</Button>
</div>
</div>
<Card>
<CardContent className="p-6 space-y-6">
<CardHeader>
<CardTitle>Email Template Configuration</CardTitle>
<CardDescription>
Configure your email template settings and content
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Email Groups */}
<div>
<Label className="text-sm font-medium text-gray-600 mb-3 block">EMAIL GROUPS</Label>

View File

@@ -0,0 +1,294 @@
"use client";
import { useState, useEffect } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useParams, useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ArrowLeft, Save, Loader2, Shield } from "lucide-react";
import { toast } from "sonner";
import { userEditFormSchema, type UserEditForm } from "@/schemas/user.schema";
import axios from "axios";
import { Header } from "@/components/common/header";
export default function UserEditPage() {
const params = useParams();
const router = useRouter();
const userId = params.id as string;
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const form = useForm<UserEditForm>({
resolver: zodResolver(userEditFormSchema),
defaultValues: {
username: "",
email: "",
firstName: "",
lastName: "",
password: "",
confirmPassword: "",
},
});
useEffect(() => {
const fetchUser = async () => {
try {
setIsLoading(true);
const response = await axios.get(`/api/user/${userId}`);
const userData = response.data;
form.reset({
username: userData.username,
email: userData.email,
firstName: userData.firstName,
lastName: userData.lastName,
password: "",
confirmPassword: "",
});
} catch (error) {
console.error("Error fetching user:", error);
toast.error("Failed to load user data");
router.push("/modules/user");
} finally {
setIsLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId, form, router]);
const onSubmit = async (data: UserEditForm) => {
setIsSubmitting(true);
try {
const updateData: {
username: string;
email: string;
firstName: string;
lastName: string;
password?: string;
} = {
username: data.username,
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
};
// Only include password if it's provided
if (data.password && data.password.length > 0) {
updateData.password = data.password;
}
const response = await axios.put(`/api/user/${userId}`, updateData);
if (response.status === 200) {
toast.success("User updated successfully!");
router.push("/modules/user");
}
} catch (error) {
console.error("Error updating user:", error);
if (axios.isAxiosError(error) && error.response?.data?.message) {
toast.error(error.response.data.message);
} else {
toast.error("Failed to update user. Please try again.");
}
} finally {
setIsSubmitting(false);
}
};
if (isLoading) {
return (
<div>
<Header />
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-center h-64">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
</div>
</div>
);
}
return (
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "User Management", href: "/modules/user" },
{ title: "Edit User", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6 space-y-6">
{/* Header */}
<div className="flex items-center space-x-4">
<Button
variant="outline"
size="sm"
onClick={() => router.push("/modules/user")}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back to Users
</Button>
<div>
<h1 className="text-2xl font-bold">Edit User</h1>
<p className="text-muted-foreground">Update user information</p>
</div>
</div>
{/* Form */}
<Card>
<CardHeader>
<CardTitle>User Information</CardTitle>
<CardDescription>
Update the user&apos;s details below. Leave password fields empty to keep current password.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="Enter first name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Enter last name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="Enter email address" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>New Password (optional)</FormLabel>
<FormControl>
<Input type="password" placeholder="Enter new password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm New Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Confirm new password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end space-x-2">
<Button
type="button"
variant="outline"
onClick={() => router.push("/modules/user")}
disabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Updating User...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Update User
</>
)}
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
{/* Permissions Management */}
<Card>
<CardHeader>
<CardTitle>Permissions</CardTitle>
<CardDescription>
Manage user permissions and access levels
</CardDescription>
</CardHeader>
<CardContent>
<Button
variant="outline"
onClick={() => router.push(`/modules/user/${userId}/permission`)}
disabled={isSubmitting}
className="flex items-center gap-2"
>
<Shield className="h-4 w-4" />
Manage Permissions
</Button>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,253 @@
"use client";
import { useState, useEffect } from "react";
import { useParams, useRouter } from "next/navigation";
import { Header } from "@/components/common/header";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { toast } from "sonner";
import { ArrowLeft, Save, Shield, User } from "lucide-react";
interface Permission {
id: number;
name: string;
description: string;
isActive: boolean;
}
interface UserPermission {
id: number;
userId: number;
permissionId: number;
permission?: Permission;
}
interface User {
id: number;
username: string;
email: string;
firstName: string;
lastName: string;
}
export default function UserPermissionPage() {
const params = useParams();
const router = useRouter();
const userId = parseInt(params.id as string);
const [user, setUser] = useState<User | null>(null);
const [permissions, setPermissions] = useState<Permission[]>([]);
const [userPermissions, setUserPermissions] = useState<UserPermission[]>([]);
const [selectedPermissions, setSelectedPermissions] = useState<Set<number>>(new Set());
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
// Fetch user data
const userResponse = await fetch(`/api/user/${userId}`);
if (!userResponse.ok) {
throw new Error("Failed to fetch user data");
}
const userData = await userResponse.json();
setUser(userData);
// Fetch all permissions
const permissionsResponse = await fetch("/api/permissions");
if (!permissionsResponse.ok) {
throw new Error("Failed to fetch permissions");
}
const permissionsData = await permissionsResponse.json();
setPermissions(permissionsData);
// Fetch user permissions
const userPermissionsResponse = await fetch(`/api/user/${userId}/permissions`);
if (!userPermissionsResponse.ok) {
throw new Error("Failed to fetch user permissions");
}
const userPermissionsData = await userPermissionsResponse.json();
setUserPermissions(userPermissionsData);
// Set selected permissions
const selectedIds: Set<number> = new Set<number>(userPermissionsData.map((up: UserPermission) => up.permissionId as number));
setSelectedPermissions(selectedIds);
} catch (error) {
console.error("Error fetching data:", error);
toast.error("Failed to load user permissions");
} finally {
setLoading(false);
}
};
fetchData();
}, [userId]);
const handlePermissionToggle = (permissionId: number) => {
const newSelected = new Set(selectedPermissions);
if (newSelected.has(permissionId)) {
newSelected.delete(permissionId);
} else {
newSelected.add(permissionId);
}
setSelectedPermissions(newSelected);
};
const handleSave = async () => {
try {
setSaving(true);
const response = await fetch(`/api/user/${userId}/permissions`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
permissionIds: Array.from(selectedPermissions),
}),
});
if (!response.ok) {
throw new Error("Failed to update permissions");
}
const updatedPermissions = await response.json();
setUserPermissions(updatedPermissions);
toast.success("User permissions updated successfully!");
} catch (error) {
console.error("Error updating permissions:", error);
toast.error("Failed to update permissions");
} finally {
setSaving(false);
}
};
const handleGoBack = () => {
router.push(`/modules/user/${userId}`);
};
if (loading) {
return (
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "User Management", href: "/modules/user" },
{ title: "Edit User", href: `/modules/user/${userId}` },
{ title: "Permissions", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6">
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="space-y-2">
<Skeleton className="h-8 w-64" />
<Skeleton className="h-4 w-96" />
</div>
<div className="flex gap-2">
<Skeleton className="h-10 w-24" />
<Skeleton className="h-10 w-24" />
</div>
</div>
<div className="grid gap-4">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} className="h-20 w-full" />
))}
</div>
</div>
</div>
</div>
);
}
return (
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "User Management", href: "/modules/user" },
{ title: "Edit User", href: `/modules/user/${userId}` },
{ title: "Permissions", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6">
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Shield className="h-6 w-6 text-primary" />
<h1 className="text-2xl font-bold">User Permissions</h1>
</div>
<div className="flex items-center gap-2 text-muted-foreground">
<User className="h-4 w-4" />
<span>
{user?.firstName} {user?.lastName} ({user?.username})
</span>
<Badge variant="outline">
{selectedPermissions.size} permission{selectedPermissions.size !== 1 ? 's' : ''} selected
</Badge>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleGoBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
Back to User
</Button>
<Button onClick={handleSave} disabled={saving}>
<Save className="h-4 w-4 mr-2" />
{saving ? "Saving..." : "Save Changes"}
</Button>
</div>
</div>
{/* Permissions Grid */}
<div className="grid gap-4">
{permissions.map((permission) => (
<Card key={permission.id} className="cursor-pointer hover:shadow-md transition-shadow">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center space-x-3">
<Checkbox
id={`permission-${permission.id}`}
checked={selectedPermissions.has(permission.id)}
onCheckedChange={() => handlePermissionToggle(permission.id)}
/>
<div className="space-y-1">
<CardTitle className="text-base">
{permission.name.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')}
</CardTitle>
<CardDescription>
{permission.description}
</CardDescription>
</div>
</div>
<Badge variant={selectedPermissions.has(permission.id) ? "default" : "secondary"}>
{selectedPermissions.has(permission.id) ? "Granted" : "Not Granted"}
</Badge>
</div>
</CardHeader>
</Card>
))}
</div>
{permissions.length === 0 && (
<Card>
<CardContent className="text-center py-8">
<Shield className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<p className="text-muted-foreground">No permissions available</p>
</CardContent>
</Card>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,215 @@
"use client";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { ArrowLeft, Save, Loader2 } from "lucide-react";
import { toast } from "sonner";
import { userFormSchema, type UserForm } from "@/schemas/user.schema";
import axios from "axios";
import { Header } from "@/components/common/header";
export default function UserAddPage() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const form = useForm<UserForm>({
resolver: zodResolver(userFormSchema),
defaultValues: {
username: "",
email: "",
firstName: "",
lastName: "",
password: "",
confirmPassword: "",
},
});
const onSubmit = async (data: UserForm) => {
setIsSubmitting(true);
try {
const response = await axios.post("/api/user", {
username: data.username,
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
password: data.password,
});
if (response.status === 201) {
toast.success("User created successfully!");
router.push("/modules/user");
}
} catch (error) {
console.error("Error creating user:", error);
if (axios.isAxiosError(error) && error.response?.data?.message) {
toast.error(error.response.data.message);
} else {
toast.error("Failed to create user. Please try again.");
}
} finally {
setIsSubmitting(false);
}
};
return (
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "User Management", href: "/modules/user" },
{ title: "Add User", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6 space-y-6">
{/* Header */}
<div className="flex items-center space-x-4">
<Button
variant="outline"
size="sm"
onClick={() => router.push("/modules/user")}
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Back to Users
</Button>
<div>
<h1 className="text-2xl font-bold">Add New User</h1>
<p className="text-muted-foreground">Create a new user account</p>
</div>
</div>
{/* Form */}
<Card>
<CardHeader>
<CardTitle>User Information</CardTitle>
<CardDescription>
Enter the user&apos;s details below to create a new account.
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="Enter first name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Enter last name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter username" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="Enter email address" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Enter password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input type="password" placeholder="Confirm password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end space-x-2">
<Button
type="button"
variant="outline"
onClick={() => router.push("/modules/user")}
disabled={isSubmitting}
>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Creating User...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Create User
</>
)}
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,233 @@
"use client";
import { useState, useEffect, useCallback, useMemo } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Plus, Loader2, Users, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import axios from "axios";
import { ServerDataTable } from "@/components/ui/server-data-table";
import { User, createUserColumns } from "@/components/users/user-columns";
import { Header } from "@/components/common/header";
interface PaginationInfo {
page: number;
pageSize: number;
total: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
interface UserResponse {
success: boolean;
data: User[];
pagination: PaginationInfo;
message?: string;
}
export default function UserPage() {
const router = useRouter();
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [query, setQuery] = useState({
page: 1,
pageSize: 10,
search: "",
sortBy: "id",
sortOrder: "asc" as "asc" | "desc",
});
const [pagination, setPagination] = useState<PaginationInfo>({
page: 1,
pageSize: 10,
total: 0,
totalPages: 0,
hasNextPage: false,
hasPreviousPage: false,
});
// Initialize search and sorting state to match query
const [searchValue, setSearchValue] = useState(query.search);
const [sorting, setSorting] = useState<{ id: string; desc: boolean }[]>(
query.sortBy ? [{ id: query.sortBy, desc: query.sortOrder === "desc" }] : []
);
const fetchUsers = useCallback(async () => {
try {
setIsLoading(true);
const params = new URLSearchParams({
page: query.page.toString(),
pageSize: query.pageSize.toString(),
search: query.search,
sortBy: query.sortBy,
sortOrder: query.sortOrder,
});
const response = await axios.get<UserResponse>(`/api/user?${params}`);
if (response.data.success) {
setUsers(response.data.data);
setPagination(response.data.pagination);
} else {
toast.error(response.data.message || "Failed to load users");
}
} catch (error) {
console.error("Error fetching users:", error);
toast.error("Failed to load users");
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, [query]);
// Fetch users when query changes
useEffect(() => {
fetchUsers();
}, [fetchUsers]);
const handleAddUser = useCallback(() => {
router.push("/modules/user/add");
}, [router]);
const handleEditUser = useCallback((userId: number) => {
router.push(`/modules/user/${userId}`);
}, [router]);
const handlePermissionsUser = useCallback((userId: number) => {
router.push(`/modules/user/${userId}/permission`);
}, [router]);
const handleDeleteUser = useCallback(async (userId: number) => {
// Find the user to get their name for confirmation
const userToDelete = users.find(u => u.id === userId);
const userName = userToDelete ? `${userToDelete.firstName} ${userToDelete.lastName}` : `User #${userId}`;
if (!window.confirm(`Are you sure you want to delete ${userName}? This action cannot be undone.`)) {
return;
}
try {
const response = await axios.delete(`/api/user/${userId}`);
if (response.data.success) {
toast.success(response.data.message || "User deleted successfully");
// Refresh the user list
await fetchUsers();
} else {
toast.error(response.data.message || "Failed to delete user");
}
} catch (error) {
console.error("Error deleting user:", error);
toast.error("Failed to delete user");
}
}, [fetchUsers, users]);
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
await fetchUsers();
toast.success("User list refreshed");
}, [fetchUsers]);
const handlePaginationChange = useCallback((page: number, pageSize: number) => {
setQuery((prev) => ({ ...prev, page, pageSize }));
}, []);
// Search handler - will be called from ServerDataTable after debounce
const handleSearchChange = useCallback((search: string) => {
setQuery((prev) => ({ ...prev, search, page: 1 }));
}, []);
// Sorting handler - will be called from ServerDataTable
const handleSortingChange = useCallback((sortByField: string, sortOrderValue: "asc" | "desc") => {
setQuery((prev) => ({ ...prev, sortBy: sortByField, sortOrder: sortOrderValue }));
setSorting([{ id: sortByField, desc: sortOrderValue === "desc" }]);
}, []);
const columns = useMemo(() => createUserColumns({
onEdit: handleEditUser,
onDelete: handleDeleteUser,
onPermissions: handlePermissionsUser,
}), [handleEditUser, handleDeleteUser, handlePermissionsUser]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="flex items-center space-x-2">
<Loader2 className="h-6 w-6 animate-spin" />
<span>Loading users...</span>
</div>
</div>
);
}
return (
<div>
<Header
breadcrumbs={[
{ title: "Home", href: "/" },
{ title: "User Management", isCurrentPage: true }
]}
/>
<div className="container mx-auto p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center space-x-2">
<Users className="h-8 w-8" />
<h1 className="text-3xl font-bold">User Management</h1>
</div>
<div className="flex items-center space-x-2">
<Button
onClick={handleRefresh}
variant="outline"
size="sm"
disabled={isRefreshing}
className="flex items-center space-x-2"
>
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
<span>Refresh</span>
</Button>
<Button onClick={handleAddUser} className="flex items-center space-x-2">
<Plus className="h-4 w-4" />
<span>Add User</span>
</Button>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Users</CardTitle>
<CardDescription>
Manage your user database ({pagination.total} total)
</CardDescription>
</CardHeader>
<CardContent>
{pagination.total > 0 ? (
<ServerDataTable
columns={columns}
data={users}
pagination={pagination}
searchKey="name"
searchPlaceholder="Search users by name..."
isLoading={isLoading}
onPaginationChange={handlePaginationChange}
onSearchChange={handleSearchChange}
onSortingChange={handleSortingChange}
searchValue={searchValue}
sorting={sorting}
setSearchValue={setSearchValue}
setSorting={setSorting}
/>
) : (
<div className="text-center py-8 text-gray-500">
<Users className="h-12 w-12 mx-auto mb-4 text-gray-400" />
<p className="text-lg font-medium">No users found</p>
<p className="text-sm">Get started by adding your first user</p>
<Button onClick={handleAddUser} className="mt-4">
<Plus className="h-4 w-4 mr-2" />
Add User
</Button>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -0,0 +1,60 @@
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@radix-ui/react-select";
interface BreadcrumbItemData {
title: string;
href?: string;
isCurrentPage?: boolean;
}
interface HeaderProps {
title?: string;
parentTitle?: string;
parentHref?: string;
showParent?: boolean;
breadcrumbs?: BreadcrumbItemData[];
}
export function Header({
title = "Data Fetching",
parentTitle = "Building Your Application",
parentHref = "#",
showParent = true,
breadcrumbs
}: HeaderProps) {
// Use breadcrumbs prop if provided, otherwise fall back to legacy props
const breadcrumbItems = breadcrumbs || [
...(showParent ? [{ title: parentTitle, href: parentHref }] : []),
{ title, isCurrentPage: true }
];
return (
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator
className="mr-2 data-[orientation=vertical]:h-4"
/>
<Breadcrumb>
<BreadcrumbList>
{breadcrumbItems.map((item, index) => (
<div key={index} className="flex items-center">
<BreadcrumbItem className={index === 0 ? "hidden md:block" : ""}>
{item.isCurrentPage ? (
<BreadcrumbPage>{item.title}</BreadcrumbPage>
) : (
<BreadcrumbLink href={item.href || "#"}>
{item.title}
</BreadcrumbLink>
)}
</BreadcrumbItem>
{index < breadcrumbItems.length - 1 && (
<BreadcrumbSeparator className="hidden md:block" />
)}
</div>
))}
</BreadcrumbList>
</Breadcrumb>
</header>
);
}

View File

@@ -0,0 +1,64 @@
import { Button } from "@/components/ui/button";
import { ShieldCheck } from "lucide-react";
export interface User {
id: number;
username: string;
email: string;
firstName: string;
lastName: string;
password: string;
isDeleted: boolean;
}
export function createUserColumns({ onEdit, onDelete, onPermissions }: {
onEdit?: (userId: number) => void;
onDelete?: (userId: number) => void;
onPermissions?: (userId: number) => void;
}) {
return [
{
accessorKey: "id",
header: "ID",
},
{
accessorKey: "fullName",
header: "Name",
cell: (row: { row: { original: User } }) => (
<button
onClick={() => onEdit?.(row.row.original.id)}
className="text-blue-600 hover:text-blue-800 hover:underline cursor-pointer font-medium"
>
{`${row.row.original.firstName} ${row.row.original.lastName}`}
</button>
),
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "username",
header: "Username",
},
{
id: "actions",
header: "Actions",
cell: (row: { row: { original: User } }) => (
<div className="flex gap-2">
{onPermissions && (
<Button size="sm" variant="outline" onClick={() => onPermissions(row.row.original.id)}>
<ShieldCheck className="h-4 w-4 mr-1" />
Permissions
</Button>
)}
{onDelete && (
<Button size="sm" variant="destructive" onClick={() => onDelete(row.row.original.id)}>
Delete
</Button>
)}
</div>
),
},
];
}

View File

@@ -2,6 +2,9 @@ import { Low } from "lowdb";
import { JSONFile } from "lowdb/node";
import { DBSchema } from "./database.schema";
import { defaultAdminUser } from "./defaultAdminUser";
import { sampleUsers } from "./sampleUsers";
import { samplePermissions } from "./samplePermissions";
import { sampleUserPermissions } from "./sampleUserPermissions";
import path from "path";
// File path for db.json
@@ -21,14 +24,52 @@ export function getNextId<T extends { id: number }>(items: T[]): number {
return Math.max(...items.map((item) => item.id)) + 1;
}
// Initialize DB and ensure default admin user exists
// Initialize DB and ensure sample data exists
export async function initDB() {
await db.read();
db.data ||= { customers: [], users: [], customerDependants: [], userPermissions: [], permissions: [] };
// Initialize users with default admin and sample users
if (db.data.users.length === 0) {
db.data.users.push(defaultAdminUser);
await db.write();
db.data.users.push(...sampleUsers);
}
// Initialize permissions with sample data
if (db.data.permissions.length === 0) {
db.data.permissions.push(...samplePermissions);
}
// Initialize user permissions with sample data
if (db.data.userPermissions.length === 0) {
db.data.userPermissions.push(...sampleUserPermissions);
}
await db.write();
}
// Reset database by clearing all data and reinitializing with sample data
export async function resetDB() {
// Clear all existing data
db.data = {
customers: [],
users: [],
customerDependants: [],
userPermissions: [],
permissions: [],
};
// Initialize users with default admin and sample users
db.data.users.push(defaultAdminUser);
db.data.users.push(...sampleUsers);
// Initialize permissions with sample data
db.data.permissions.push(...samplePermissions);
// Initialize user permissions with sample data
db.data.userPermissions.push(...sampleUserPermissions);
await db.write();
}
export { db };

View File

@@ -1,93 +1,5 @@
{
"customers": [
{
"id": 6,
"firstNameEn": "David",
"lastNameEn": "Brown",
"email": "david.brown@example.com",
"mobile": "0987901234",
"originAdd1": "sadfsa",
"localAdd1": "sadfsaf"
},
{
"id": 7,
"firstNameEn": "Emily",
"lastNameEn": "Davis",
"email": "emily.davis@example.com",
"mobile": "0987567890",
"originAdd1": "sadfsa",
"localAdd1": "asdfsaf"
},
{
"id": 8,
"firstNameEn": "Robert",
"lastNameEn": "Miller",
"email": "robert.miller@example.com",
"mobile": "0987234567"
},
{
"id": 9,
"firstNameEn": "Lisa",
"lastNameEn": "Wilson",
"email": "lisa.wilson@example.com",
"mobile": "0987890123",
"originAdd1": "ssss",
"localAdd1": "sssss"
},
{
"id": 10,
"firstNameEn": "James",
"lastNameEn": "Moore",
"email": "james.moore@example.com",
"mobile": "0987456789"
},
{
"id": 11,
"firstNameEn": "Ashley",
"lastNameEn": "Taylor",
"email": "ashley.taylor@example.com",
"mobile": "0987012345"
},
{
"id": 12,
"firstNameEn": "Christopher",
"lastNameEn": "Anderson",
"email": "christopher.anderson@example.com",
"mobile": "0987678901"
},
{
"id": 13,
"firstNameEn": "sdfas",
"lastNameEn": "sadfasf",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491"
},
{
"id": 14,
"firstNameEn": "sdaf",
"lastNameEn": "asdfasf",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491"
},
{
"id": 15,
"firstNameEn": "Đỗ",
"lastNameEn": "Thanh Tùng",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491",
"originAdd1": "123",
"localAdd1": "asdfsdaf"
},
{
"id": 16,
"firstNameEn": "Đỗ",
"lastNameEn": "Thanh Tùng",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491",
"originAdd1": "12344444",
"localAdd1": "asdfsdaf11111"
}
],
"customers": [],
"users": [
{
"id": 1,
@@ -97,52 +9,490 @@
"lastName": "User",
"password": "admin123",
"isDeleted": false
}
],
"customerDependants": [
},
{
"id": 2,
"username": "john.doe",
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe",
"password": "password123",
"isDeleted": false
},
{
"id": 3,
"username": "jane.smith",
"email": "jane.smith@example.com",
"firstName": "Jane",
"lastName": "Smith",
"password": "password123",
"isDeleted": false
},
{
"id": 4,
"username": "mike.johnson",
"email": "mike.johnson@example.com",
"firstName": "Mike",
"lastName": "Johnson",
"password": "password123",
"isDeleted": false
},
{
"id": 5,
"custId": 13,
"firstNameEn": "do thanh",
"lastNameEn": "tung",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491"
"username": "sarah.wilson",
"email": "sarah.wilson@example.com",
"firstName": "Sarah",
"lastName": "Wilson",
"password": "password123",
"isDeleted": false
},
{
"id": 6,
"custId": 13,
"firstNameEn": "do thanh",
"lastNameEn": "tung",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417492"
"username": "david.brown",
"email": "david.brown@example.com",
"firstName": "David",
"lastName": "Brown",
"password": "password123",
"isDeleted": false
},
{
"id": 1751787328700,
"custId": 14,
"firstNameEn": "222",
"lastNameEn": "3333",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491"
"id": 7,
"username": "emma.davis",
"email": "emma.davis@example.com",
"firstName": "Emma",
"lastName": "Davis",
"password": "password123",
"isDeleted": false
},
{
"id": 1751787328701,
"custId": 15,
"firstNameEn": "Đỗ",
"lastNameEn": "Thanh Tùng",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491",
"originAdd1": "123",
"localAdd1": "sdfgsdfg"
"id": 8,
"username": "alex.martinez",
"email": "alex.martinez@example.com",
"firstName": "Alex",
"lastName": "Martinez",
"password": "password123",
"isDeleted": false
},
{
"id": 1751787328702,
"custId": 16,
"firstNameEn": "Đỗ",
"lastNameEn": "Tùng",
"originAdd1": "1231231231",
"localAdd1": "asdfsdaf",
"id": 9,
"username": "lisa.garcia",
"email": "lisa.garcia@example.com",
"firstName": "Lisa",
"lastName": "Garcia",
"password": "password123",
"isDeleted": false
},
{
"id": 10,
"username": "robert.taylor",
"email": "robert.taylor@example.com",
"firstName": "Robert",
"lastName": "Taylor",
"password": "password123",
"isDeleted": false
},
{
"id": 11,
"username": "maria.rodriguez",
"email": "maria.rodriguez@example.com",
"firstName": "Maria",
"lastName": "Rodriguez",
"password": "password123",
"isDeleted": false
},
{
"id": 12,
"username": "tungdt",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491"
"firstName": "Đỗ",
"lastName": "Tùng",
"password": "123456",
"isDeleted": false
}
]
],
"userPermissions": [
{
"id": 16,
"userId": 2,
"permissionId": 4
},
{
"id": 17,
"userId": 2,
"permissionId": 5
},
{
"id": 18,
"userId": 2,
"permissionId": 8
},
{
"id": 19,
"userId": 2,
"permissionId": 11
},
{
"id": 20,
"userId": 3,
"permissionId": 1
},
{
"id": 21,
"userId": 3,
"permissionId": 2
},
{
"id": 22,
"userId": 3,
"permissionId": 14
},
{
"id": 23,
"userId": 3,
"permissionId": 8
},
{
"id": 24,
"userId": 4,
"permissionId": 4
},
{
"id": 25,
"userId": 4,
"permissionId": 1
},
{
"id": 26,
"userId": 4,
"permissionId": 8
},
{
"id": 27,
"userId": 4,
"permissionId": 9
},
{
"id": 28,
"userId": 4,
"permissionId": 15
},
{
"id": 29,
"userId": 5,
"permissionId": 4
},
{
"id": 30,
"userId": 5,
"permissionId": 5
},
{
"id": 31,
"userId": 5,
"permissionId": 11
},
{
"id": 32,
"userId": 5,
"permissionId": 12
},
{
"id": 33,
"userId": 6,
"permissionId": 7
},
{
"id": 34,
"userId": 6,
"permissionId": 10
},
{
"id": 35,
"userId": 6,
"permissionId": 15
},
{
"id": 36,
"userId": 6,
"permissionId": 1
},
{
"id": 37,
"userId": 6,
"permissionId": 2
},
{
"id": 38,
"userId": 7,
"permissionId": 11
},
{
"id": 39,
"userId": 7,
"permissionId": 12
},
{
"id": 40,
"userId": 7,
"permissionId": 13
},
{
"id": 41,
"userId": 7,
"permissionId": 4
},
{
"id": 42,
"userId": 8,
"permissionId": 4
},
{
"id": 43,
"userId": 8,
"permissionId": 1
},
{
"id": 44,
"userId": 8,
"permissionId": 11
},
{
"id": 45,
"userId": 9,
"permissionId": 4
},
{
"id": 46,
"userId": 9,
"permissionId": 5
},
{
"id": 47,
"userId": 9,
"permissionId": 6
},
{
"id": 48,
"userId": 9,
"permissionId": 8
},
{
"id": 49,
"userId": 9,
"permissionId": 11
},
{
"id": 50,
"userId": 10,
"permissionId": 4
},
{
"id": 51,
"userId": 10,
"permissionId": 1
},
{
"id": 52,
"userId": 10,
"permissionId": 8
},
{
"id": 53,
"userId": 10,
"permissionId": 9
},
{
"id": 54,
"userId": 11,
"permissionId": 4
},
{
"id": 55,
"userId": 11,
"permissionId": 5
},
{
"id": 56,
"userId": 11,
"permissionId": 8
},
{
"id": 57,
"userId": 11,
"permissionId": 11
},
{
"id": 58,
"userId": 11,
"permissionId": 12
},
{
"id": 59,
"userId": 1,
"permissionId": 1
},
{
"id": 60,
"userId": 1,
"permissionId": 2
},
{
"id": 61,
"userId": 1,
"permissionId": 4
},
{
"id": 62,
"userId": 1,
"permissionId": 5
},
{
"id": 63,
"userId": 1,
"permissionId": 6
},
{
"id": 64,
"userId": 1,
"permissionId": 7
},
{
"id": 65,
"userId": 1,
"permissionId": 8
},
{
"id": 66,
"userId": 1,
"permissionId": 9
},
{
"id": 67,
"userId": 1,
"permissionId": 10
},
{
"id": 68,
"userId": 1,
"permissionId": 11
},
{
"id": 69,
"userId": 1,
"permissionId": 12
},
{
"id": 70,
"userId": 1,
"permissionId": 13
},
{
"id": 71,
"userId": 1,
"permissionId": 14
},
{
"id": 72,
"userId": 1,
"permissionId": 15
},
{
"id": 73,
"userId": 1,
"permissionId": 3
}
],
"permissions": [
{
"id": 1,
"name": "user_read",
"description": "Read user information",
"isActive": true
},
{
"id": 2,
"name": "user_write",
"description": "Create and update user information",
"isActive": true
},
{
"id": 3,
"name": "user_delete",
"description": "Delete user accounts",
"isActive": true
},
{
"id": 4,
"name": "customer_read",
"description": "Read customer information",
"isActive": true
},
{
"id": 5,
"name": "customer_write",
"description": "Create and update customer information",
"isActive": true
},
{
"id": 6,
"name": "customer_delete",
"description": "Delete customer records",
"isActive": true
},
{
"id": 7,
"name": "admin_panel",
"description": "Access administrative panel",
"isActive": true
},
{
"id": 8,
"name": "reports_view",
"description": "View system reports",
"isActive": true
},
{
"id": 9,
"name": "reports_export",
"description": "Export reports and data",
"isActive": true
},
{
"id": 10,
"name": "system_settings",
"description": "Modify system configuration",
"isActive": true
},
{
"id": 11,
"name": "mail_template_read",
"description": "Read mail templates",
"isActive": true
},
{
"id": 12,
"name": "mail_template_write",
"description": "Create and update mail templates",
"isActive": true
},
{
"id": 13,
"name": "mail_template_delete",
"description": "Delete mail templates",
"isActive": true
},
{
"id": 14,
"name": "permission_manage",
"description": "Manage user permissions",
"isActive": true
},
{
"id": 15,
"name": "audit_log",
"description": "View audit logs and system activities",
"isActive": true
}
],
"customerDependants": []
}

View File

@@ -0,0 +1,94 @@
import { Permission } from "./database.schema";
export const samplePermissions: Permission[] = [
{
id: 1,
name: "user_read",
description: "Read user information",
isActive: true,
},
{
id: 2,
name: "user_write",
description: "Create and update user information",
isActive: true,
},
{
id: 3,
name: "user_delete",
description: "Delete user accounts",
isActive: true,
},
{
id: 4,
name: "customer_read",
description: "Read customer information",
isActive: true,
},
{
id: 5,
name: "customer_write",
description: "Create and update customer information",
isActive: true,
},
{
id: 6,
name: "customer_delete",
description: "Delete customer records",
isActive: true,
},
{
id: 7,
name: "admin_panel",
description: "Access administrative panel",
isActive: true,
},
{
id: 8,
name: "reports_view",
description: "View system reports",
isActive: true,
},
{
id: 9,
name: "reports_export",
description: "Export reports and data",
isActive: true,
},
{
id: 10,
name: "system_settings",
description: "Modify system configuration",
isActive: true,
},
{
id: 11,
name: "mail_template_read",
description: "Read mail templates",
isActive: true,
},
{
id: 12,
name: "mail_template_write",
description: "Create and update mail templates",
isActive: true,
},
{
id: 13,
name: "mail_template_delete",
description: "Delete mail templates",
isActive: true,
},
{
id: 14,
name: "permission_manage",
description: "Manage user permissions",
isActive: true,
},
{
id: 15,
name: "audit_log",
description: "View audit logs and system activities",
isActive: true,
},
];

View File

@@ -0,0 +1,83 @@
import { UserPermission } from "./database.schema";
export const sampleUserPermissions: UserPermission[] = [
// Admin user (id: 1) gets all permissions
{ id: 1, userId: 1, permissionId: 1 },
{ id: 2, userId: 1, permissionId: 2 },
{ id: 3, userId: 1, permissionId: 3 },
{ id: 4, userId: 1, permissionId: 4 },
{ id: 5, userId: 1, permissionId: 5 },
{ id: 6, userId: 1, permissionId: 6 },
{ id: 7, userId: 1, permissionId: 7 },
{ id: 8, userId: 1, permissionId: 8 },
{ id: 9, userId: 1, permissionId: 9 },
{ id: 10, userId: 1, permissionId: 10 },
{ id: 11, userId: 1, permissionId: 11 },
{ id: 12, userId: 1, permissionId: 12 },
{ id: 13, userId: 1, permissionId: 13 },
{ id: 14, userId: 1, permissionId: 14 },
{ id: 15, userId: 1, permissionId: 15 },
// John Doe (id: 2) - Customer Manager
{ id: 16, userId: 2, permissionId: 4 }, // customer_read
{ id: 17, userId: 2, permissionId: 5 }, // customer_write
{ id: 18, userId: 2, permissionId: 8 }, // reports_view
{ id: 19, userId: 2, permissionId: 11 }, // mail_template_read
// Jane Smith (id: 3) - User Manager
{ id: 20, userId: 3, permissionId: 1 }, // user_read
{ id: 21, userId: 3, permissionId: 2 }, // user_write
{ id: 22, userId: 3, permissionId: 14 }, // permission_manage
{ id: 23, userId: 3, permissionId: 8 }, // reports_view
// Mike Johnson (id: 4) - Reports Analyst
{ id: 24, userId: 4, permissionId: 4 }, // customer_read
{ id: 25, userId: 4, permissionId: 1 }, // user_read
{ id: 26, userId: 4, permissionId: 8 }, // reports_view
{ id: 27, userId: 4, permissionId: 9 }, // reports_export
{ id: 28, userId: 4, permissionId: 15 }, // audit_log
// Sarah Wilson (id: 5) - Customer Support
{ id: 29, userId: 5, permissionId: 4 }, // customer_read
{ id: 30, userId: 5, permissionId: 5 }, // customer_write
{ id: 31, userId: 5, permissionId: 11 }, // mail_template_read
{ id: 32, userId: 5, permissionId: 12 }, // mail_template_write
// David Brown (id: 6) - System Administrator
{ id: 33, userId: 6, permissionId: 7 }, // admin_panel
{ id: 34, userId: 6, permissionId: 10 }, // system_settings
{ id: 35, userId: 6, permissionId: 15 }, // audit_log
{ id: 36, userId: 6, permissionId: 1 }, // user_read
{ id: 37, userId: 6, permissionId: 2 }, // user_write
// Emma Davis (id: 7) - Content Manager
{ id: 38, userId: 7, permissionId: 11 }, // mail_template_read
{ id: 39, userId: 7, permissionId: 12 }, // mail_template_write
{ id: 40, userId: 7, permissionId: 13 }, // mail_template_delete
{ id: 41, userId: 7, permissionId: 4 }, // customer_read
// Alex Martinez (id: 8) - Junior Developer
{ id: 42, userId: 8, permissionId: 4 }, // customer_read
{ id: 43, userId: 8, permissionId: 1 }, // user_read
{ id: 44, userId: 8, permissionId: 11 }, // mail_template_read
// Lisa Garcia (id: 9) - Senior Support
{ id: 45, userId: 9, permissionId: 4 }, // customer_read
{ id: 46, userId: 9, permissionId: 5 }, // customer_write
{ id: 47, userId: 9, permissionId: 6 }, // customer_delete
{ id: 48, userId: 9, permissionId: 8 }, // reports_view
{ id: 49, userId: 9, permissionId: 11 }, // mail_template_read
// Robert Taylor (id: 10) - Data Analyst
{ id: 50, userId: 10, permissionId: 4 }, // customer_read
{ id: 51, userId: 10, permissionId: 1 }, // user_read
{ id: 52, userId: 10, permissionId: 8 }, // reports_view
{ id: 53, userId: 10, permissionId: 9 }, // reports_export
// Maria Rodriguez (id: 11) - Customer Service Lead
{ id: 54, userId: 11, permissionId: 4 }, // customer_read
{ id: 55, userId: 11, permissionId: 5 }, // customer_write
{ id: 56, userId: 11, permissionId: 8 }, // reports_view
{ id: 57, userId: 11, permissionId: 11 }, // mail_template_read
{ id: 58, userId: 11, permissionId: 12 }, // mail_template_write
];

View File

@@ -0,0 +1,94 @@
import { User } from "./database.schema";
export const sampleUsers: User[] = [
{
id: 2,
username: "john.doe",
email: "john.doe@example.com",
firstName: "John",
lastName: "Doe",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 3,
username: "jane.smith",
email: "jane.smith@example.com",
firstName: "Jane",
lastName: "Smith",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 4,
username: "mike.johnson",
email: "mike.johnson@example.com",
firstName: "Mike",
lastName: "Johnson",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 5,
username: "sarah.wilson",
email: "sarah.wilson@example.com",
firstName: "Sarah",
lastName: "Wilson",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 6,
username: "david.brown",
email: "david.brown@example.com",
firstName: "David",
lastName: "Brown",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 7,
username: "emma.davis",
email: "emma.davis@example.com",
firstName: "Emma",
lastName: "Davis",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 8,
username: "alex.martinez",
email: "alex.martinez@example.com",
firstName: "Alex",
lastName: "Martinez",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 9,
username: "lisa.garcia",
email: "lisa.garcia@example.com",
firstName: "Lisa",
lastName: "Garcia",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 10,
username: "robert.taylor",
email: "robert.taylor@example.com",
firstName: "Robert",
lastName: "Taylor",
password: "password123", // You should hash this in production
isDeleted: false,
},
{
id: 11,
username: "maria.rodriguez",
email: "maria.rodriguez@example.com",
firstName: "Maria",
lastName: "Rodriguez",
password: "password123", // You should hash this in production
isDeleted: false,
},
];

View File

@@ -0,0 +1,41 @@
import { z } from "zod";
export const userFormSchema = z.object({
username: z.string().min(1, "Username is required").min(3, "Username must be at least 3 characters"),
email: z.string().email("Invalid email address"),
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
password: z.string().min(6, "Password must be at least 6 characters"),
confirmPassword: z.string().min(6, "Confirm password must be at least 6 characters"),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
export const userEditFormSchema = z.object({
username: z.string().min(1, "Username is required").min(3, "Username must be at least 3 characters"),
email: z.string().email("Invalid email address"),
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
password: z.string().optional(),
confirmPassword: z.string().optional(),
}).refine((data) => {
if (data.password && data.password.length > 0) {
return data.password.length >= 6;
}
return true;
}, {
message: "Password must be at least 6 characters",
path: ["password"],
}).refine((data) => {
if (data.password && data.password.length > 0) {
return data.password === data.confirmPassword;
}
return true;
}, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
export type UserForm = z.infer<typeof userFormSchema>;
export type UserEditForm = z.infer<typeof userEditFormSchema>;