Add user page
This commit is contained in:
@@ -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();
|
||||
|
||||
18
src/app/api/permissions/route.ts
Normal file
18
src/app/api/permissions/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
234
src/app/api/user/[id]/permissions/route.ts
Normal file
234
src/app/api/user/[id]/permissions/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
172
src/app/api/user/[id]/route.ts
Normal file
172
src/app/api/user/[id]/route.ts
Normal 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
150
src/app/api/user/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
294
src/app/modules/user/[id]/page.tsx
Normal file
294
src/app/modules/user/[id]/page.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
253
src/app/modules/user/[id]/permission/page.tsx
Normal file
253
src/app/modules/user/[id]/permission/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
215
src/app/modules/user/add/page.tsx
Normal file
215
src/app/modules/user/add/page.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
233
src/app/modules/user/page.tsx
Normal file
233
src/app/modules/user/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
60
src/components/common/header.tsx
Normal file
60
src/components/common/header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
64
src/components/users/user-columns.tsx
Normal file
64
src/components/users/user-columns.tsx
Normal 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>
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
94
src/database/samplePermissions.ts
Normal file
94
src/database/samplePermissions.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
83
src/database/sampleUserPermissions.ts
Normal file
83
src/database/sampleUserPermissions.ts
Normal 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
|
||||
];
|
||||
94
src/database/sampleUsers.ts
Normal file
94
src/database/sampleUsers.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
41
src/schemas/user.schema.ts
Normal file
41
src/schemas/user.schema.ts
Normal 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>;
|
||||
Reference in New Issue
Block a user