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

Compare commits

..

8 Commits

32 changed files with 2540 additions and 958 deletions

3
.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.git
.next
node_modules

32
Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# Stage 1: Build
FROM node:18-slim AS builder
WORKDIR /app
# Copy dependencies first to cache better
COPY package.json package-lock.json ./
RUN npm install
# Copy all source code
COPY . .
# Build the Next.js app
RUN npm run build
# Stage 2: Run production image
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/package.json /app/package-lock.json ./
RUN npm install --omit=dev
COPY --from=builder /app/.next .next
COPY --from=builder /app/public public
COPY --from=builder /app/next.config.ts ./
COPY --from=builder /app/src src
EXPOSE 3000
CMD ["npx", "next", "start"]

View File

@@ -2,35 +2,37 @@ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-
## Getting Started
First, run the development server:
Environment requirements
- NodeJS version 22 or latest
- npm version 10.9.2 or latest
Environment variable
- Make .env.local file in `root` folder
```env
JWT_SECRET=your secret key
```
Install package
```bash
npm install
```
Run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
npm run dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Application features
- Login
- username: `admin`
- password: `admin123`
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
- Customer features
- User featues
- Mail template features
- Logout

12
docker-compose.yaml Normal file
View File

@@ -0,0 +1,12 @@
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: cosmos-crm
ports:
- "3000:3000"
environment:
- JWT_SECRET=8f2e9c4a7b1d6e3f8a5b2c9e6f1a4d7b8e3f6a9c2d5e8b1f4a7c0e3d6f9b2a5c8

61
middleware.ts Normal file
View File

@@ -0,0 +1,61 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Define protected routes
const protectedRoutes = ['/modules'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Check if the current path is a protected route
const isProtectedRoute = protectedRoutes.some(route =>
pathname.startsWith(route)
);
// Get token from cookies
const token = request.cookies.get('auth-token')?.value;
// If accessing a protected route without a token, redirect to login
if (isProtectedRoute && !token) {
return NextResponse.redirect(new URL('/auth/login', request.url));
}
// Verify token if it exists
if (token) {
try {
jwt.verify(token, JWT_SECRET);
// If user is authenticated and trying to access login page, redirect to modules
if (pathname === '/auth/login') {
return NextResponse.redirect(new URL('/modules/user', request.url));
}
} catch {
// Invalid token - remove it and redirect to login if accessing protected route
const response = NextResponse.redirect(new URL('/auth/login', request.url));
response.cookies.delete('auth-token');
return response;
}
}
// Allow the request to continue
return NextResponse.next();
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};

159
package-lock.json generated
View File

@@ -22,10 +22,14 @@
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-table": "^8.21.3",
"@types/js-cookie": "^3.0.6",
"@types/jsonwebtoken": "^9.0.10",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"lowdb": "^7.0.1",
"lucide-react": "^0.525.0",
"next": "15.3.5",
@@ -2297,6 +2301,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/js-cookie": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -2311,6 +2321,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
@@ -2328,11 +2348,16 @@
"@types/lodash": "*"
}
},
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.19.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.4.tgz",
"integrity": "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -3260,6 +3285,12 @@
"node": ">=8"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -3680,6 +3711,15 @@
"node": ">= 0.4"
}
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@@ -5269,6 +5309,15 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-cookie": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -5323,6 +5372,28 @@
"json5": "lib/cli.js"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.2",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
"license": "MIT",
"dependencies": {
"jws": "^3.2.2",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -5339,6 +5410,27 @@
"node": ">=4.0"
}
},
"node_modules/jwa": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5638,6 +5730,42 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -5645,6 +5773,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@@ -5812,7 +5946,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -6570,6 +6703,26 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/safe-push-apply": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
@@ -6615,7 +6768,6 @@
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"devOptional": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -7342,7 +7494,6 @@
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unrs-resolver": {

View File

@@ -23,10 +23,14 @@
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-table": "^8.21.3",
"@types/js-cookie": "^3.0.6",
"@types/jsonwebtoken": "^9.0.10",
"axios": "^1.10.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"lowdb": "^7.0.1",
"lucide-react": "^0.525.0",
"next": "15.3.5",

View File

@@ -1,5 +1,8 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, initDB, resetDB } from '@/database/database';
import { db, initDB } from '@/database/database';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
export async function POST(req: NextRequest) {
await initDB();
@@ -8,8 +11,30 @@ export async function POST(req: NextRequest) {
(u) => u.username === username && u.password === password && !u.isDeleted
);
if (user) {
// In a real app, return a JWT or session token
return NextResponse.json({ success: true, user: { id: user.id, username: user.username, email: user.email, firstName: user.firstName, lastName: user.lastName } });
// Create JWT token
const token = jwt.sign(
{
id: user.id.toString(),
username: user.username,
email: user.email
},
JWT_SECRET,
{ expiresIn: '1h' }
);
const userData = {
id: user.id.toString(),
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
};
return NextResponse.json({
success: true,
token,
user: userData
});
} else {
return NextResponse.json({ success: false, message: 'Invalid credentials' }, { status: 401 });
}

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, initDB } from '@/database/database';
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
export async function POST(req: NextRequest) {
try {
await initDB();
const { token } = await req.json();
if (!token) {
return NextResponse.json({ success: false, message: 'No token provided' }, { status: 401 });
}
// Verify the token
const decoded = jwt.verify(token, JWT_SECRET) as { id: string; username: string; email: string };
// Find the user in the database
const user = db.data?.users.find(
(u) => u.id.toString() === decoded.id && !u.isDeleted
);
if (!user) {
return NextResponse.json({ success: false, message: 'User not found' }, { status: 401 });
}
const userData = {
id: user.id.toString(),
username: user.username,
email: user.email,
firstName: user.firstName,
lastName: user.lastName
};
return NextResponse.json({
success: true,
user: userData
});
} catch {
return NextResponse.json({ success: false, message: 'Invalid token' }, { status: 401 });
}
}

View File

@@ -3,14 +3,14 @@ import { db } from "@/database/database";
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Initialize database
await db.read();
// Parse dependant ID from params
const dependantId = parseInt(params.id);
const dependantId = parseInt((await params).id);
if (isNaN(dependantId)) {
return NextResponse.json(
@@ -61,14 +61,14 @@ export async function GET(
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Initialize database
await db.read();
// Parse dependant ID from params
const dependantId = parseInt(params.id);
const dependantId = parseInt((await params).id);
if (isNaN(dependantId)) {
return NextResponse.json(
@@ -104,8 +104,8 @@ export async function PUT(
...db.data!.customerDependants[dependantIndex],
firstNameEn: body.dependantInfo.firstNameEn,
lastNameEn: body.dependantInfo.lastNameEn,
originAdd1: body.dependantInfo.originAdd1,
localAdd1: body.dependantInfo.localAdd1,
originAdd1: body.dependantContact.originAdd1,
localAdd1: body.dependantContact.localAdd1,
email: body.dependantContact.email,
mobile: body.dependantContact.mobile,
};
@@ -141,14 +141,14 @@ export async function PUT(
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
// Initialize database
await db.read();
// Parse dependant ID from params
const dependantId = parseInt(params.id);
const dependantId = parseInt((await params).id);
if (isNaN(dependantId)) {
return NextResponse.json(

View File

@@ -116,10 +116,17 @@ export async function PUT(
...db.data!.customers[customerIndex],
firstNameEn: body.customerInfo.firstNameEn,
lastNameEn: body.customerInfo.lastNameEn,
originAdd1: body.customerInfo.originAdd1,
localAdd1: body.customerInfo.localAdd1,
firstNameJp: body.customerInfo.firstNameJp,
lastNameJp: body.customerInfo.lastNameJp,
firstNameJpKana: body.customerInfo.firstNameJpKana,
lastNameJpKana: body.customerInfo.lastNameJpKana,
gender: body.customerInfo.gender,
martial: body.customerInfo.martial,
status: body.customerInfo.status,
email: body.customerContact.email,
mobile: body.customerContact.mobile,
originAdd1: body.customerContact.originAdd1,
localAdd1: body.customerContact.localAdd1,
};
db.data!.customers[customerIndex] = updatedCustomer;
@@ -140,10 +147,14 @@ export async function PUT(
custId: customerId,
firstNameEn: dependant.dependantInfo.firstNameEn,
lastNameEn: dependant.dependantInfo.lastNameEn,
originAdd1: dependant.dependantInfo.originAdd1,
localAdd1: dependant.dependantInfo.localAdd1,
firstNameJp: dependant.dependantInfo.firstNameJp,
lastNameJp: dependant.dependantInfo.lastNameJp,
firstNameJpKana: dependant.dependantInfo.firstNameJpKana,
lastNameJpKana: dependant.dependantInfo.lastNameJpKana,
email: dependant.dependantContact.email,
mobile: dependant.dependantContact.mobile,
originAdd1: dependant.dependantContact.originAdd1,
localAdd1: dependant.dependantContact.localAdd1,
};
newDependants.push(newDependant);
db.data!.customerDependants.push(newDependant);

View File

@@ -19,10 +19,17 @@ export async function POST(request: NextRequest) {
id: getNextId(db.data!.customers),
firstNameEn: validatedData.customerInfo.firstNameEn,
lastNameEn: validatedData.customerInfo.lastNameEn,
firstNameJp: validatedData.customerInfo.firstNameJp,
lastNameJp: validatedData.customerInfo.lastNameJp,
firstNameJpKana: validatedData.customerInfo.firstNameJpKana,
lastNameJpKana: validatedData.customerInfo.lastNameJpKana,
gender: validatedData.customerInfo.gender,
martial: validatedData.customerInfo.martial,
status: validatedData.customerInfo.status,
email: validatedData.customerContact.email,
mobile: validatedData.customerContact.mobile,
originAdd1: validatedData.customerInfo.originAdd1,
localAdd1: validatedData.customerInfo.localAdd1,
originAdd1: validatedData.customerContact.originAdd1,
localAdd1: validatedData.customerContact.localAdd1,
};
// Add customer to database
@@ -38,10 +45,14 @@ export async function POST(request: NextRequest) {
custId: newCustomer.id,
firstNameEn: dependant.dependantInfo.firstNameEn,
lastNameEn: dependant.dependantInfo.lastNameEn,
firstNameJp: dependant.dependantInfo.firstNameJp,
lastNameJp: dependant.dependantInfo.lastNameJp,
firstNameJpKana: dependant.dependantInfo.firstNameJpKana,
lastNameJpKana: dependant.dependantInfo.lastNameJpKana,
email: dependant.dependantContact.email,
mobile: dependant.dependantContact.mobile,
originAdd1: dependant.dependantInfo.originAdd1,
localAdd1: dependant.dependantInfo.localAdd1,
originAdd1: dependant.dependantContact.originAdd1,
localAdd1: dependant.dependantContact.localAdd1,
};
customerDependants.push(newDependant);
db.data!.customerDependants.push(newDependant);
@@ -102,6 +113,9 @@ export async function GET(request: NextRequest) {
const page = parseInt(searchParams.get("page") || "1");
const pageSize = parseInt(searchParams.get("pageSize") || "10");
const search = searchParams.get("search") || "";
const gender = searchParams.get("gender") || "";
const martial = searchParams.get("martial") || "";
const status = searchParams.get("status") || "";
const sortBy = searchParams.get("sortBy") || "id";
const sortOrder = searchParams.get("sortOrder") || "asc";
@@ -129,6 +143,27 @@ export async function GET(request: NextRequest) {
);
}
// Apply gender filter
if (gender) {
customersWithDependants = customersWithDependants.filter((customer) =>
customer.gender.toLowerCase() === gender.toLowerCase()
);
}
// Apply martial status filter
if (martial) {
customersWithDependants = customersWithDependants.filter((customer) =>
customer.martial.toLowerCase() === martial.toLowerCase()
);
}
// Apply status filter
if (status) {
customersWithDependants = customersWithDependants.filter((customer) =>
customer.status.toLowerCase() === status.toLowerCase()
);
}
// Apply sorting
customersWithDependants.sort((a, b) => {
let aValue: string | number = a[sortBy as keyof typeof a] as string | number;

View File

@@ -13,10 +13,11 @@ function writeDB() {
// GET /api/user/[id]/permissions - Get all permissions for a user
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
const userId = parseInt(params.id);
const { id } = await params;
const userId = parseInt(id);
if (isNaN(userId)) {
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });
@@ -51,10 +52,11 @@ export async function GET(
// POST /api/user/[id]/permissions - Add permissions to a user
export async function POST(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
const userId = parseInt(params.id);
const { id } = await params;
const userId = parseInt(id);
if (isNaN(userId)) {
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });
@@ -126,10 +128,11 @@ export async function POST(
// PUT /api/user/[id]/permissions - Update permissions for a user
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
const userId = parseInt(params.id);
const { id } = await params;
const userId = parseInt(id);
if (isNaN(userId)) {
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });
@@ -201,10 +204,11 @@ export async function PUT(
// DELETE /api/user/[id]/permissions - Remove all permissions from a user
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: Promise<{ id: string }> }
) {
try {
const userId = parseInt(params.id);
const { id } = await params;
const userId = parseInt(id);
if (isNaN(userId)) {
return NextResponse.json({ error: "Invalid user ID" }, { status: 400 });

View File

@@ -4,8 +4,10 @@ import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import axios from "axios";
import { useState } from "react";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { loginSchema } from "@/schemas/auth.schema";
import { useAuth } from "@/contexts/AuthContext";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@@ -19,6 +21,15 @@ export default function LoginPage() {
});
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const { login, isAuthenticated } = useAuth();
const router = useRouter();
// Redirect if already authenticated
useEffect(() => {
if (isAuthenticated) {
router.push('/modules/user');
}
}, [isAuthenticated, router]);
const onSubmit = async (data: LoginForm) => {
setError("");
@@ -26,13 +37,15 @@ export default function LoginPage() {
try {
const res = await axios.post("/api/auth", data);
if (res.data.success) {
// You can redirect or set session here
window.location.href = "/modules/user"; // Redirect to user page on successful login
// Use the authentication context to login
login(res.data.token, res.data.user);
router.push("/modules/user");
} else {
setError(res.data.message || "Login failed");
}
} catch (e: any) {
setError(e.response?.data?.message || "Login failed");
} catch (e: unknown) {
const error = e as { response?: { data?: { message?: string } } };
setError(error.response?.data?.message || "Login failed");
} finally {
setLoading(false);
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader } from "@/components/ui/card";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
import { Plus, Trash2, ArrowLeft } from "lucide-react";
@@ -35,12 +36,19 @@ export default function CustomerAddPage() {
customerInfo: {
firstNameEn: "",
lastNameEn: "",
originAdd1: "",
localAdd1: "",
firstNameJp: "",
lastNameJp: "",
firstNameJpKana: "",
lastNameJpKana: "",
gender: "",
martial: "",
status: "",
},
customerContact: {
email: "",
mobile: "",
originAdd1: "",
localAdd1: "",
},
customerDependants: [],
},
@@ -53,12 +61,16 @@ export default function CustomerAddPage() {
dependantInfo: {
firstNameEn: "",
lastNameEn: "",
originAdd1: "",
localAdd1: "",
firstNameJp: "",
lastNameJp: "",
firstNameJpKana: "",
lastNameJpKana: "",
},
dependantContact: {
email: "",
mobile: "",
originAdd1: "",
localAdd1: "",
},
},
});
@@ -102,12 +114,16 @@ export default function CustomerAddPage() {
dependantInfo: {
firstNameEn: data.dependantInfo.firstNameEn,
lastNameEn: data.dependantInfo.lastNameEn,
originAdd1: data.dependantInfo.originAdd1,
localAdd1: data.dependantInfo.localAdd1,
firstNameJp: data.dependantInfo.firstNameJp,
lastNameJp: data.dependantInfo.lastNameJp,
firstNameJpKana: data.dependantInfo.firstNameJpKana,
lastNameJpKana: data.dependantInfo.lastNameJpKana,
},
dependantContact: {
email: data.dependantContact.email,
mobile: data.dependantContact.mobile,
originAdd1: data.dependantContact.originAdd1,
localAdd1: data.dependantContact.localAdd1,
},
};
@@ -139,8 +155,13 @@ export default function CustomerAddPage() {
const isValid = await form.trigger([
"customerInfo.firstNameEn",
"customerInfo.lastNameEn",
"customerInfo.originAdd1",
"customerInfo.localAdd1"
"customerInfo.firstNameJp",
"customerInfo.lastNameJp",
"customerInfo.firstNameJpKana",
"customerInfo.lastNameJpKana",
"customerInfo.gender",
"customerInfo.martial",
"customerInfo.status"
]);
if (isValid) {
setActiveTab("contact");
@@ -149,7 +170,12 @@ export default function CustomerAddPage() {
}
} else if (activeTab === "contact") {
// Validate customer contact fields before proceeding
const isValid = await form.trigger(["customerContact.email", "customerContact.mobile"]);
const isValid = await form.trigger([
"customerContact.email",
"customerContact.mobile",
"customerContact.originAdd1",
"customerContact.localAdd1"
]);
if (isValid) {
setActiveTab("dependants");
} else {
@@ -174,19 +200,33 @@ export default function CustomerAddPage() {
return (
values.customerInfo?.firstNameEn?.trim() !== "" &&
values.customerInfo?.lastNameEn?.trim() !== "" &&
values.customerInfo?.originAdd1?.trim() !== "" &&
values.customerInfo?.localAdd1?.trim() !== "" &&
values.customerInfo?.firstNameJp?.trim() !== "" &&
values.customerInfo?.lastNameJp?.trim() !== "" &&
values.customerInfo?.firstNameJpKana?.trim() !== "" &&
values.customerInfo?.lastNameJpKana?.trim() !== "" &&
values.customerInfo?.gender?.trim() !== "" &&
values.customerInfo?.martial?.trim() !== "" &&
values.customerInfo?.status?.trim() !== "" &&
!errors.customerInfo?.firstNameEn &&
!errors.customerInfo?.lastNameEn &&
!errors.customerInfo?.originAdd1 &&
!errors.customerInfo?.localAdd1
!errors.customerInfo?.firstNameJp &&
!errors.customerInfo?.lastNameJp &&
!errors.customerInfo?.firstNameJpKana &&
!errors.customerInfo?.lastNameJpKana &&
!errors.customerInfo?.gender &&
!errors.customerInfo?.martial &&
!errors.customerInfo?.status
);
} else if (tabName === "contact") {
return (
values.customerContact?.email?.trim() !== "" &&
values.customerContact?.mobile?.trim() !== "" &&
values.customerContact?.originAdd1?.trim() !== "" &&
values.customerContact?.localAdd1?.trim() !== "" &&
!errors.customerContact?.email &&
!errors.customerContact?.mobile
!errors.customerContact?.mobile &&
!errors.customerContact?.originAdd1 &&
!errors.customerContact?.localAdd1
);
}
return true;
@@ -276,16 +316,14 @@ export default function CustomerAddPage() {
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="customerInfo.originAdd1"
name="customerInfo.firstNameJp"
render={({ field }) => (
<FormItem>
<FormLabel>Origin Address</FormLabel>
<FormLabel>First Name (Japanese)</FormLabel>
<FormControl>
<Input placeholder="Enter origin address" {...field} />
<Input placeholder="名前を入力してください" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -293,17 +331,110 @@ export default function CustomerAddPage() {
/>
<FormField
control={form.control}
name="customerInfo.localAdd1"
name="customerInfo.lastNameJp"
render={({ field }) => (
<FormItem>
<FormLabel>Local Address</FormLabel>
<FormLabel>Last Name (Japanese)</FormLabel>
<FormControl>
<Input placeholder="Enter local address" {...field} />
<Input placeholder="姓を入力してください" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="customerInfo.firstNameJpKana"
render={({ field }) => (
<FormItem>
<FormLabel>First Name (Japanese Kana)</FormLabel>
<FormControl>
<Input placeholder="ナマエ" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="customerInfo.lastNameJpKana"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name (Japanese Kana)</FormLabel>
<FormControl>
<Input placeholder="セイ" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="customerInfo.gender"
render={({ field }) => (
<FormItem>
<FormLabel>Gender</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select gender" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="male">Male</SelectItem>
<SelectItem value="female">Female</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="customerInfo.martial"
render={({ field }) => (
<FormItem>
<FormLabel>Marital Status</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select marital status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="single">Single</SelectItem>
<SelectItem value="married">Married</SelectItem>
<SelectItem value="divorced">Divorced</SelectItem>
<SelectItem value="widowed">Widowed</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="customerInfo.status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end">
<Button type="button" onClick={handleNextTab}>
@@ -342,6 +473,34 @@ export default function CustomerAddPage() {
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={form.control}
name="customerContact.originAdd1"
render={({ field }) => (
<FormItem>
<FormLabel>Origin Address</FormLabel>
<FormControl>
<Input placeholder="Enter origin address" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="customerContact.localAdd1"
render={({ field }) => (
<FormItem>
<FormLabel>Local Address</FormLabel>
<FormControl>
<Input placeholder="Enter local address" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-between">
<Button type="button" variant="outline" onClick={handlePreviousTab}>
Previous
@@ -403,16 +562,14 @@ export default function CustomerAddPage() {
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={dependantForm.control}
name="dependantInfo.originAdd1"
name="dependantInfo.firstNameJp"
render={({ field }) => (
<FormItem>
<FormLabel>Origin Address</FormLabel>
<FormLabel>First Name (Japanese)</FormLabel>
<FormControl>
<Input placeholder="Enter origin address" {...field} />
<Input placeholder="名前を入力してください" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -420,12 +577,38 @@ export default function CustomerAddPage() {
/>
<FormField
control={dependantForm.control}
name="dependantInfo.localAdd1"
name="dependantInfo.lastNameJp"
render={({ field }) => (
<FormItem>
<FormLabel>Local Address</FormLabel>
<FormLabel>Last Name (Japanese)</FormLabel>
<FormControl>
<Input placeholder="Enter local address" {...field} />
<Input placeholder="姓を入力してください" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={dependantForm.control}
name="dependantInfo.firstNameJpKana"
render={({ field }) => (
<FormItem>
<FormLabel>First Name (Japanese Kana)</FormLabel>
<FormControl>
<Input placeholder="ナマエ" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={dependantForm.control}
name="dependantInfo.lastNameJpKana"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name (Japanese Kana)</FormLabel>
<FormControl>
<Input placeholder="セイ" {...field} />
</FormControl>
<FormMessage />
</FormItem>
@@ -439,8 +622,10 @@ export default function CustomerAddPage() {
const isValid = await dependantForm.trigger([
"dependantInfo.firstNameEn",
"dependantInfo.lastNameEn",
"dependantInfo.originAdd1",
"dependantInfo.localAdd1"
"dependantInfo.firstNameJp",
"dependantInfo.lastNameJp",
"dependantInfo.firstNameJpKana",
"dependantInfo.lastNameJpKana"
]);
if (isValid) {
setDependantDialogTab("contact");
@@ -483,6 +668,34 @@ export default function CustomerAddPage() {
)}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<FormField
control={dependantForm.control}
name="dependantContact.originAdd1"
render={({ field }) => (
<FormItem>
<FormLabel>Origin Address</FormLabel>
<FormControl>
<Input placeholder="Enter origin address" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={dependantForm.control}
name="dependantContact.localAdd1"
render={({ field }) => (
<FormItem>
<FormLabel>Local Address</FormLabel>
<FormControl>
<Input placeholder="Enter local address" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-start">
<Button
type="button"
@@ -544,8 +757,8 @@ export default function CustomerAddPage() {
<TableCell>{dependant.dependantInfo.lastNameEn}</TableCell>
<TableCell>{dependant.dependantContact.email}</TableCell>
<TableCell>{dependant.dependantContact.mobile}</TableCell>
<TableCell>{dependant.dependantInfo.originAdd1}</TableCell>
<TableCell>{dependant.dependantInfo.localAdd1}</TableCell>
<TableCell>{dependant.dependantContact.originAdd1}</TableCell>
<TableCell>{dependant.dependantContact.localAdd1}</TableCell>
<TableCell>
<Button
type="button"

View File

@@ -4,7 +4,10 @@ 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 { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Label } from "@/components/ui/label";
import { Plus, Loader2, Users, FileText, Download, FileSpreadsheet, Filter, X } from "lucide-react";
import { toast } from "sonner";
import axios from "axios";
import { ServerDataTable } from "@/components/ui/server-data-table";
@@ -31,11 +34,13 @@ export default function CustomerPage() {
const router = useRouter();
const [customers, setCustomers] = useState<Customer[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isRefreshing, setIsRefreshing] = useState(false);
const [query, setQuery] = useState({
page: 1,
pageSize: 10,
search: "",
gender: "",
martial: "",
status: "",
sortBy: "id",
sortOrder: "asc" as "asc" | "desc",
});
@@ -47,8 +52,12 @@ export default function CustomerPage() {
hasNextPage: false,
hasPreviousPage: false,
});
// Initialize search and sorting state to match query
const [searchValue, setSearchValue] = useState(query.search);
// Individual filter states
const [nameSearch, setNameSearch] = useState("");
const [genderFilter, setGenderFilter] = useState("all");
const [martialFilter, setMartialFilter] = useState("all");
const [statusFilter, setStatusFilter] = useState("all");
// Initialize sorting state to match query
const [sorting, setSorting] = useState<{ id: string; desc: boolean }[]>(
query.sortBy ? [{ id: query.sortBy, desc: query.sortOrder === "desc" }] : []
);
@@ -60,6 +69,9 @@ export default function CustomerPage() {
page: query.page.toString(),
pageSize: query.pageSize.toString(),
search: query.search,
gender: query.gender,
martial: query.martial,
status: query.status,
sortBy: query.sortBy,
sortOrder: query.sortOrder,
});
@@ -75,7 +87,6 @@ export default function CustomerPage() {
toast.error("Failed to load customers");
} finally {
setIsLoading(false);
setIsRefreshing(false);
}
}, [query]);
@@ -116,18 +127,50 @@ export default function CustomerPage() {
}
}, [fetchCustomers, customers]);
const handleRefresh = useCallback(async () => {
setIsRefreshing(true);
await fetchCustomers();
toast.success("Customer list refreshed");
}, [fetchCustomers]);
const handlePaginationChange = useCallback((page: number, pageSize: number) => {
setQuery((prev) => ({ ...prev, page, pageSize }));
}, []);
// Search handler - will be called from ServerDataTable after debounce
// Filter handlers - only update local state, don't trigger search
const handleNameSearchChange = useCallback((value: string) => {
setNameSearch(value);
}, []);
const handleGenderFilterChange = useCallback((value: string) => {
setGenderFilter(value);
}, []);
const handleMartialFilterChange = useCallback((value: string) => {
setMartialFilter(value);
}, []);
const handleStatusFilterChange = useCallback((value: string) => {
setStatusFilter(value);
}, []);
// Apply filters when filter button is clicked
const handleApplyFilters = useCallback(() => {
setQuery((prev) => ({
...prev,
search: nameSearch,
gender: genderFilter === "all" ? "" : genderFilter,
martial: martialFilter === "all" ? "" : martialFilter,
status: statusFilter === "all" ? "" : statusFilter,
page: 1
}));
}, [nameSearch, genderFilter, martialFilter, statusFilter]);
const handleClearFilters = useCallback(() => {
setNameSearch("");
setGenderFilter("all");
setMartialFilter("all");
setStatusFilter("all");
setQuery((prev) => ({ ...prev, search: "", gender: "", martial: "", status: "", page: 1 }));
}, []);
// Legacy search handler for ServerDataTable compatibility
const handleSearchChange = useCallback((search: string) => {
setNameSearch(search);
setQuery((prev) => ({ ...prev, search, page: 1 }));
}, []);
@@ -169,19 +212,47 @@ export default function CustomerPage() {
</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>
<FileText className="h-4 w-4" />
<span>PDF Export</span>
</Button>
<Button onClick={handleAddCustomer} className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
className="flex items-center space-x-2"
>
<Download className="h-4 w-4" />
<span>CSV Download</span>
</Button>
<Button
variant="outline"
size="sm"
className="flex items-center space-x-2"
>
<FileSpreadsheet className="h-4 w-4" />
<span>Excel Download</span>
</Button>
<Button
onClick={handleAddCustomer}
variant="outline"
size="sm"
className="flex items-center space-x-2"
>
<Plus className="h-4 w-4" />
<span>Add Customer</span>
</Button>
<Button
onClick={handleApplyFilters}
variant="outline"
size="sm"
className="flex items-center space-x-2"
>
<Filter className="h-4 w-4" />
<span>Filter</span>
</Button>
</div>
</div>
@@ -193,20 +264,95 @@ export default function CustomerPage() {
</CardDescription>
</CardHeader>
<CardContent>
{/* Filter Section */}
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Search & Filter</h3>
<Button
variant="outline"
size="sm"
onClick={handleClearFilters}
className="flex items-center space-x-2"
>
<X className="h-4 w-4" />
<span>Clear All</span>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{/* Name Search */}
<div className="space-y-2">
<Label htmlFor="nameSearch">Search by Name</Label>
<Input
id="nameSearch"
placeholder="Enter customer name..."
value={nameSearch}
onChange={(e) => handleNameSearchChange(e.target.value)}
/>
</div>
{/* Gender Filter */}
<div className="space-y-2">
<Label htmlFor="genderFilter">Gender</Label>
<Select value={genderFilter} onValueChange={handleGenderFilterChange}>
<SelectTrigger id="genderFilter">
<SelectValue placeholder="Select gender" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Genders</SelectItem>
<SelectItem value="male">Male</SelectItem>
<SelectItem value="female">Female</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>
{/* Marital Status Filter */}
<div className="space-y-2">
<Label htmlFor="martialFilter">Marital Status</Label>
<Select value={martialFilter} onValueChange={handleMartialFilterChange}>
<SelectTrigger id="martialFilter">
<SelectValue placeholder="Select marital status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="single">Single</SelectItem>
<SelectItem value="married">Married</SelectItem>
<SelectItem value="divorced">Divorced</SelectItem>
<SelectItem value="widowed">Widowed</SelectItem>
</SelectContent>
</Select>
</div>
{/* Status Filter */}
<div className="space-y-2">
<Label htmlFor="statusFilter">Status</Label>
<Select value={statusFilter} onValueChange={handleStatusFilterChange}>
<SelectTrigger id="statusFilter">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="inactive">Inactive</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
{pagination.total > 0 ? (
<ServerDataTable
columns={columns}
data={customers}
pagination={pagination}
searchKey="name"
searchPlaceholder="Search customers by name..."
isLoading={isLoading}
onPaginationChange={handlePaginationChange}
onSearchChange={handleSearchChange}
onSortingChange={handleSortingChange}
searchValue={searchValue}
searchValue={nameSearch}
sorting={sorting}
setSearchValue={setSearchValue}
setSearchValue={setNameSearch}
setSorting={setSorting}
/>
) : (

View File

@@ -1,4 +1,5 @@
import { AppSidebar } from "@/components/sidebar/app-sidebar";
import { ProtectedRoute } from "@/components/common/ProtectedRoute";
import {
SidebarInset,
SidebarProvider,
@@ -10,6 +11,7 @@ export default function ModulesLayout({
children: React.ReactNode;
}>) {
return (
<ProtectedRoute>
<main>
<SidebarProvider>
<AppSidebar />
@@ -18,5 +20,6 @@ export default function ModulesLayout({
</SidebarInset>
</SidebarProvider>
</main>
</ProtectedRoute>
);
}

View File

@@ -1,103 +1,30 @@
import Image from "next/image";
"use client";
import { useAuth } from "@/contexts/AuthContext";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
const { isAuthenticated, isLoading } = useAuth();
const router = useRouter();
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
useEffect(() => {
if (!isLoading) {
if (isAuthenticated) {
router.push('/modules/user');
} else {
router.push('/auth/login');
}
}
}, [isAuthenticated, isLoading, router]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
</div>
);
}
return null;
}

View File

@@ -0,0 +1,34 @@
"use client";
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
interface ProtectedRouteProps {
children: React.ReactNode;
}
export const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
const { isAuthenticated, isLoading } = useAuth();
const router = useRouter();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/auth/login');
}
}, [isAuthenticated, isLoading, router]);
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-gray-900"></div>
</div>
);
}
if (!isAuthenticated) {
return null;
}
return <>{children}</>;
};

View File

@@ -1,6 +1,12 @@
"use client";
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator } from "@/components/ui/breadcrumb";
import { SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@radix-ui/react-select";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { useAuth } from "@/contexts/AuthContext";
import { LogOut, User } from "lucide-react";
interface BreadcrumbItemData {
title: string;
@@ -23,6 +29,8 @@ export function Header({
showParent = true,
breadcrumbs
}: HeaderProps) {
const { user, logout } = useAuth();
// Use breadcrumbs prop if provided, otherwise fall back to legacy props
const breadcrumbItems = breadcrumbs || [
...(showParent ? [{ title: parentTitle, href: parentHref }] : []),
@@ -55,6 +63,36 @@ export function Header({
))}
</BreadcrumbList>
</Breadcrumb>
{/* User Dropdown */}
<div className="ml-auto">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="relative h-8 w-8 rounded-full">
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-gray-200">
<User className="h-4 w-4" />
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="end" forceMount>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">
{user?.firstName} {user?.lastName}
</p>
<p className="text-xs leading-none text-muted-foreground">
{user?.email}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={logout} className="cursor-pointer">
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
);
}

View File

@@ -14,6 +14,9 @@ export interface Customer {
mobile: string;
originAdd1: string;
localAdd1: string;
gender: string;
martial: string;
status: string;
dependants?: Array<{
id: number;
custId: number;
@@ -109,6 +112,87 @@ export const createCustomerColumns = ({
return <div className="font-mono">{row.getValue("mobile")}</div>;
},
},
{
accessorKey: "gender",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="h-8 px-2 lg:px-3"
>
Gender
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const gender = row.getValue("gender") as string;
return (
<Badge variant="outline" className="capitalize">
{gender || "Not specified"}
</Badge>
);
},
},
{
accessorKey: "martial",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="h-8 px-2 lg:px-3"
>
Marital Status
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const martial = row.getValue("martial") as string;
return (
<Badge variant="outline" className="capitalize">
{martial || "Not specified"}
</Badge>
);
},
},
{
accessorKey: "status",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className="h-8 px-2 lg:px-3"
>
Status
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
cell: ({ row }) => {
const status = row.getValue("status") as string;
const getStatusVariant = (status: string) => {
switch (status?.toLowerCase()) {
case "active":
return "default";
case "inactive":
return "secondary";
case "pending":
return "outline";
default:
return "outline";
}
};
return (
<Badge variant={getStatusVariant(status)} className="capitalize">
{status || "Not specified"}
</Badge>
);
},
},
{
id: "dependants",
header: "Dependants",

View File

@@ -1,5 +1,5 @@
import { Button } from "@/components/ui/button";
import { Key, ArrowUpDown } from "lucide-react";
import { Key, ArrowUpDown, Trash2 } from "lucide-react";
import { Column } from "@tanstack/react-table";
export interface User {
@@ -102,7 +102,13 @@ export function createUserColumns({ onEdit, onDelete, onPermissions }: {
</Button>
)}
{onDelete && (
<Button size="sm" variant="destructive" onClick={() => onDelete(row.row.original.id)}>
<Button
size="sm"
variant="outline"
onClick={() => onDelete(row.row.original.id)}
className="text-red-600 hover:text-red-800 hover:bg-red-50"
>
<Trash2 className="h-4 w-4 mr-2" />
Delete
</Button>
)}

View File

@@ -0,0 +1,101 @@
"use client";
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
import Cookies from 'js-cookie';
import { useRouter } from 'next/navigation';
interface User {
id: string;
username: string;
email: string;
firstName: string;
lastName: string;
}
interface AuthContextType {
user: User | null;
login: (token: string, userData: User) => void;
logout: () => void;
isAuthenticated: boolean;
isLoading: boolean;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
interface AuthProviderProps {
children: ReactNode;
}
export const AuthProvider = ({ children }: AuthProviderProps) => {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
const login = (token: string, userData: User) => {
Cookies.set('auth-token', token, { expires: 7 }); // 7 days
setUser(userData);
};
const logout = () => {
Cookies.remove('auth-token');
setUser(null);
router.push('/auth/login');
};
const verifyToken = async (token: string) => {
try {
const response = await fetch('/api/auth/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
});
if (response.ok) {
const data = await response.json();
if (data.success) {
setUser(data.user);
return true;
}
}
return false;
} catch (error) {
console.error('Token verification failed:', error);
return false;
}
};
useEffect(() => {
const initAuth = async () => {
const token = Cookies.get('auth-token');
if (token) {
const isValid = await verifyToken(token);
if (!isValid) {
Cookies.remove('auth-token');
}
}
setIsLoading(false);
};
initAuth();
}, []);
const value = {
user,
login,
logout,
isAuthenticated: !!user,
isLoading,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};

View File

@@ -4,10 +4,10 @@ export interface Customer {
id: number;
firstNameEn: string;
lastNameEn: string;
// firstNameJp: string;
// lastNameJp: string;
// firstNameJpKana: string;
// lastNameJpKana: string;
firstNameJp: string;
lastNameJp: string;
firstNameJpKana: string;
lastNameJpKana: string;
email: string;
// email2?: string;
mobile: string;
@@ -27,8 +27,8 @@ export interface Customer {
// localCountry: string;
// preferLang: string;
// dob: string; // ISO string
// gender: string;
// martial: string;
gender: string;
martial: string;
// occupation: string;
// ecName: string;
// ecRelation: string;
@@ -38,7 +38,7 @@ export interface Customer {
// initialOutReachRemark?: string;
// consentGiven?: string;
// contentDate?: string;
// status: string;
status: string;
// remarks: string;
// potcustId?: string;
// isDeleted: boolean;
@@ -73,10 +73,10 @@ export interface CustomerDependant {
// deptSeq: number;
firstNameEn: string;
lastNameEn: string;
// firstNameJp: string;
// lastNameJp: string;
// firstNameJpKana: string;
// lastNameJpKana: string;
firstNameJp: string;
lastNameJp: string;
firstNameJpKana: string;
lastNameJpKana: string;
email: string;
// email2?: string;
mobile: string;

View File

@@ -4,46 +4,257 @@
"id": 1,
"firstNameEn": "John",
"lastNameEn": "Smith",
"firstNameJp": "ジョン",
"lastNameJp": "スミス",
"firstNameJpKana": "ジョン",
"lastNameJpKana": "スミス",
"email": "john.smith@email.com",
"mobile": "+1-555-0101",
"originAdd1": "123 Main Street, New York, NY 10001, USA",
"localAdd1": "456 Oak Avenue, Tokyo, Japan 150-0001"
"localAdd1": "456 Oak Avenue, Tokyo, Japan 150-0001",
"gender": "other",
"martial": "married",
"status": "active"
},
{
"id": 2,
"firstNameEn": "Sarah",
"lastNameEn": "Johnson",
"firstNameJp": "サラ",
"lastNameJp": "ジョンソン",
"firstNameJpKana": "サラ",
"lastNameJpKana": "ジョンソン",
"email": "sarah.johnson@email.com",
"mobile": "+1-555-0102",
"originAdd1": "789 Pine Road, Los Angeles, CA 90210, USA",
"localAdd1": "321 Sakura Street, Osaka, Japan 530-0001"
"localAdd1": "321 Sakura Street, Osaka, Japan 530-0001",
"gender": "female",
"martial": "single",
"status": "active"
},
{
"id": 3,
"firstNameEn": "Michael",
"lastNameEn": "Brown",
"firstNameJp": "マイケル",
"lastNameJp": "ブラウン",
"firstNameJpKana": "マイケル",
"lastNameJpKana": "ブラウン",
"email": "michael.brown@email.com",
"mobile": "+44-20-7946-0958",
"originAdd1": "42 Victoria Street, London, UK SW1H 0TL",
"localAdd1": "789 Shibuya Crossing, Tokyo, Japan 150-0002"
"localAdd1": "789 Shibuya Crossing, Tokyo, Japan 150-0002",
"gender": "male",
"martial": "divorced",
"status": "pending"
},
{
"id": 4,
"firstNameEn": "Emily",
"lastNameEn": "Davis",
"firstNameJp": "エミリー",
"lastNameJp": "デイビス",
"firstNameJpKana": "エミリー",
"lastNameJpKana": "デイビス",
"email": "emily.davis@email.com",
"mobile": "+61-2-9876-5432",
"originAdd1": "15 Collins Street, Melbourne, VIC 3000, Australia",
"localAdd1": "654 Namba District, Osaka, Japan 542-0076"
"localAdd1": "654 Namba District, Osaka, Japan 542-0076",
"gender": "female",
"martial": "married",
"status": "active"
},
{
"id": 5,
"firstNameEn": "David",
"lastNameEn": "Wilson",
"firstNameJp": "デイビッド",
"lastNameJp": "ウィルソン",
"firstNameJpKana": "デイビッド",
"lastNameJpKana": "ウィルソン",
"email": "david.wilson@email.com",
"mobile": "+1-416-555-0199",
"originAdd1": "100 Queen Street West, Toronto, ON M5H 2N2, Canada",
"localAdd1": "987 Ginza District, Tokyo, Japan 104-0061"
"localAdd1": "987 Ginza District, Tokyo, Japan 104-0061",
"gender": "male",
"martial": "single",
"status": "inactive"
},
{
"id": 6,
"firstNameEn": "Maria",
"lastNameEn": "Garcia",
"firstNameJp": "マリア",
"lastNameJp": "ガルシア",
"firstNameJpKana": "マリア",
"lastNameJpKana": "ガルシア",
"email": "maria.garcia@email.com",
"mobile": "+34-91-555-0123",
"originAdd1": "Calle Gran Vía 45, Madrid, Spain 28013",
"localAdd1": "246 Harajuku Street, Tokyo, Japan 150-0001",
"gender": "female",
"martial": "married",
"status": "active"
},
{
"id": 7,
"firstNameEn": "James",
"lastNameEn": "Lee",
"firstNameJp": "ジェームス",
"lastNameJp": "リー",
"firstNameJpKana": "ジェームス",
"lastNameJpKana": "リー",
"email": "james.lee@email.com",
"mobile": "+65-6555-0187",
"originAdd1": "1 Raffles Place, Singapore 048616",
"localAdd1": "135 Roppongi Hills, Tokyo, Japan 106-6108",
"gender": "male",
"martial": "single",
"status": "active"
},
{
"id": 8,
"firstNameEn": "Anna",
"lastNameEn": "Mueller",
"firstNameJp": "アンナ",
"lastNameJp": "ミュラー",
"firstNameJpKana": "アンナ",
"lastNameJpKana": "ミュラー",
"email": "anna.mueller@email.com",
"mobile": "+49-30-555-0234",
"originAdd1": "Unter den Linden 77, Berlin, Germany 10117",
"localAdd1": "567 Shinjuku District, Tokyo, Japan 160-0022",
"gender": "female",
"martial": "widowed",
"status": "pending"
},
{
"id": 9,
"firstNameEn": "Robert",
"lastNameEn": "Taylor",
"firstNameJp": "ロバート",
"lastNameJp": "テイラー",
"firstNameJpKana": "ロバート",
"lastNameJpKana": "テイラー",
"email": "robert.taylor@email.com",
"mobile": "+1-555-0145",
"originAdd1": "890 Broadway, New York, NY 10003, USA",
"localAdd1": "890 Akasaka District, Tokyo, Japan 107-0052",
"gender": "male",
"martial": "married",
"status": "inactive"
},
{
"id": 10,
"firstNameEn": "Lisa",
"lastNameEn": "Anderson",
"firstNameJp": "リサ",
"lastNameJp": "アンダーソン",
"firstNameJpKana": "リサ",
"lastNameJpKana": "アンダーソン",
"email": "lisa.anderson@email.com",
"mobile": "+1-555-0167",
"originAdd1": "456 Sunset Boulevard, Los Angeles, CA 90028, USA",
"localAdd1": "234 Ikebukuro District, Tokyo, Japan 171-0021",
"gender": "female",
"martial": "divorced",
"status": "active"
},
{
"id": 11,
"firstNameEn": "Alex",
"lastNameEn": "Chen",
"firstNameJp": "アレックス",
"lastNameJp": "チェン",
"firstNameJpKana": "アレックス",
"lastNameJpKana": "チェン",
"email": "alex.chen@email.com",
"mobile": "+86-21-555-0189",
"originAdd1": "88 Nanjing Road, Shanghai, China 200001",
"localAdd1": "678 Aoyama District, Tokyo, Japan 107-0061",
"gender": "other",
"martial": "single",
"status": "active"
},
{
"id": 12,
"firstNameEn": "Sophie",
"lastNameEn": "Martin",
"firstNameJp": "ソフィー",
"lastNameJp": "マルタン",
"firstNameJpKana": "ソフィー",
"lastNameJpKana": "マルタン",
"email": "sophie.martin@email.com",
"mobile": "+33-1-555-0212",
"originAdd1": "25 Champs-Élysées, Paris, France 75008",
"localAdd1": "345 Ebisu District, Tokyo, Japan 150-0013",
"gender": "female",
"martial": "married",
"status": "pending"
},
{
"id": 13,
"firstNameEn": "Thomas",
"lastNameEn": "White",
"firstNameJp": "トーマス",
"lastNameJp": "ホワイト",
"firstNameJpKana": "トーマス",
"lastNameJpKana": "ホワイト",
"email": "thomas.white@email.com",
"mobile": "+1-555-0234",
"originAdd1": "789 Michigan Avenue, Chicago, IL 60611, USA",
"localAdd1": "456 Kanda District, Tokyo, Japan 101-0047",
"gender": "male",
"martial": "single",
"status": "active"
},
{
"id": 14,
"firstNameEn": "Isabella",
"lastNameEn": "Rodriguez",
"firstNameJp": "イザベラ",
"lastNameJp": "ロドリゲス",
"firstNameJpKana": "イザベラ",
"lastNameJpKana": "ロドリゲス",
"email": "isabella.rodriguez@email.com",
"mobile": "+52-55-555-0256",
"originAdd1": "Avenida Reforma 123, Mexico City, Mexico 06600",
"localAdd1": "789 Ueno District, Tokyo, Japan 110-0005",
"gender": "female",
"martial": "married",
"status": "inactive"
},
{
"id": 15,
"firstNameEn": "William",
"lastNameEn": "Thompson",
"firstNameJp": "ウィリアム",
"lastNameJp": "トンプソン",
"firstNameJpKana": "ウィリアム",
"lastNameJpKana": "トンプソン",
"email": "william.thompson@email.com",
"mobile": "+1-555-0278",
"originAdd1": "567 Market Street, San Francisco, CA 94105, USA",
"localAdd1": "123 Sumida District, Tokyo, Japan 130-0013",
"gender": "male",
"martial": "divorced",
"status": "active"
},
{
"id": 16,
"firstNameEn": "Do",
"lastNameEn": "Tung",
"firstNameJp": "ádf",
"lastNameJp": "ádf",
"firstNameJpKana": "ádf",
"lastNameJpKana": "ádf",
"gender": "female",
"martial": "married",
"status": "active",
"email": "dothanhtung196@gmail.com",
"mobile": "0987417491",
"originAdd1": "1231231231",
"localAdd1": "asdfsdaf"
}
],
"users": [
@@ -190,260 +401,160 @@
},
{
"id": 9,
"userId": 1,
"permissionId": 9
"userId": 2,
"permissionId": 5
},
{
"id": 10,
"userId": 1,
"permissionId": 10
"userId": 2,
"permissionId": 6
},
{
"id": 11,
"userId": 1,
"permissionId": 11
"userId": 2,
"permissionId": 8
},
{
"id": 12,
"userId": 1,
"permissionId": 12
},
{
"id": 13,
"userId": 1,
"permissionId": 13
},
{
"id": 14,
"userId": 1,
"permissionId": 14
},
{
"id": 15,
"userId": 1,
"permissionId": 15
},
{
"id": 20,
"userId": 3,
"permissionId": 1
},
{
"id": 21,
"id": 13,
"userId": 3,
"permissionId": 2
},
{
"id": 22,
"id": 14,
"userId": 3,
"permissionId": 14
"permissionId": 4
},
{
"id": 23,
"userId": 3,
"id": 15,
"userId": 4,
"permissionId": 5
},
{
"id": 16,
"userId": 4,
"permissionId": 6
},
{
"id": 17,
"userId": 4,
"permissionId": 7
},
{
"id": 18,
"userId": 4,
"permissionId": 1
},
{
"id": 19,
"userId": 5,
"permissionId": 5
},
{
"id": 20,
"userId": 5,
"permissionId": 6
},
{
"id": 21,
"userId": 5,
"permissionId": 8
},
{
"id": 22,
"userId": 6,
"permissionId": 1
},
{
"id": 23,
"userId": 6,
"permissionId": 2
},
{
"id": 24,
"userId": 4,
"userId": 6,
"permissionId": 4
},
{
"id": 25,
"userId": 4,
"permissionId": 1
"userId": 6,
"permissionId": 8
},
{
"id": 26,
"userId": 4,
"userId": 7,
"permissionId": 8
},
{
"id": 27,
"userId": 4,
"permissionId": 9
"userId": 7,
"permissionId": 5
},
{
"id": 28,
"userId": 4,
"permissionId": 15
"userId": 8,
"permissionId": 5
},
{
"id": 29,
"userId": 5,
"permissionId": 4
"userId": 8,
"permissionId": 1
},
{
"id": 30,
"userId": 5,
"userId": 9,
"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,
"id": 32,
"userId": 9,
"permissionId": 7
},
{
"id": 33,
"userId": 9,
"permissionId": 8
},
{
"id": 49,
"userId": 9,
"permissionId": 11
},
{
"id": 50,
"id": 34,
"userId": 10,
"permissionId": 4
"permissionId": 5
},
{
"id": 51,
"id": 35,
"userId": 10,
"permissionId": 1
},
{
"id": 52,
"userId": 10,
"permissionId": 8
},
{
"id": 53,
"userId": 10,
"permissionId": 9
},
{
"id": 54,
"userId": 11,
"permissionId": 4
},
{
"id": 55,
"id": 36,
"userId": 11,
"permissionId": 5
},
{
"id": 56,
"id": 37,
"userId": 11,
"permissionId": 6
},
{
"id": 38,
"userId": 11,
"permissionId": 8
},
{
"id": 57,
"userId": 11,
"permissionId": 11
},
{
"id": 58,
"userId": 11,
"permissionId": 12
},
{
"id": 59,
"userId": 2,
"permissionId": 4
},
{
"id": 60,
"userId": 2,
"permissionId": 5
},
{
"id": 61,
"userId": 2,
"permissionId": 8
},
{
"id": 62,
"userId": 2,
"permissionId": 11
}
],
"permissions": [
{
"id": 1,
"name": "user_read",
"description": "Read user information",
"description": "View user list and user details",
"isActive": true
},
{
@@ -460,103 +571,45 @@
},
{
"id": 4,
"name": "customer_read",
"description": "Read customer information",
"name": "user_permissions",
"description": "Manage user permissions",
"isActive": true
},
{
"id": 5,
"name": "customer_read",
"description": "View customer list and customer details",
"isActive": true
},
{
"id": 6,
"name": "customer_write",
"description": "Create and update customer information",
"isActive": true
},
{
"id": 6,
"id": 7,
"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",
"name": "mail_template_access",
"description": "Access mail template configuration",
"isActive": true
}
],
"customerDependants": [
{
"id": 1,
"custId": 1,
"firstNameEn": "Jane",
"lastNameEn": "Smith",
"email": "jane.smith@email.com",
"mobile": "+1-555-0111",
"originAdd1": "123 Main Street, New York, NY 10001, USA",
"localAdd1": "456 Oak Avenue, Tokyo, Japan 150-0001"
},
{
"id": 2,
"custId": 1,
"firstNameEn": "Tommy",
"lastNameEn": "Smith",
"email": "tommy.smith@email.com",
"mobile": "+1-555-0112",
"originAdd1": "123 Main Street, New York, NY 10001, USA",
"localAdd1": "456 Oak Avenue, Tokyo, Japan 150-0001"
},
{
"id": 3,
"custId": 2,
"firstNameEn": "Mark",
"lastNameEn": "Johnson",
"firstNameJp": "マーク",
"lastNameJp": "ジョンソン",
"firstNameJpKana": "マーク",
"lastNameJpKana": "ジョンソン",
"email": "mark.johnson@email.com",
"mobile": "+1-555-0121",
"originAdd1": "789 Pine Road, Los Angeles, CA 90210, USA",
@@ -567,6 +620,10 @@
"custId": 2,
"firstNameEn": "Lisa",
"lastNameEn": "Johnson",
"firstNameJp": "リサ",
"lastNameJp": "ジョンソン",
"firstNameJpKana": "リサ",
"lastNameJpKana": "ジョンソン",
"email": "lisa.johnson@email.com",
"mobile": "+1-555-0122",
"originAdd1": "789 Pine Road, Los Angeles, CA 90210, USA",
@@ -577,6 +634,10 @@
"custId": 3,
"firstNameEn": "Anna",
"lastNameEn": "Brown",
"firstNameJp": "アンナ",
"lastNameJp": "ブラウン",
"firstNameJpKana": "アンナ",
"lastNameJpKana": "ブラウン",
"email": "anna.brown@email.com",
"mobile": "+44-20-7946-0959",
"originAdd1": "42 Victoria Street, London, UK SW1H 0TL",
@@ -587,6 +648,10 @@
"custId": 4,
"firstNameEn": "James",
"lastNameEn": "Davis",
"firstNameJp": "ジェームズ",
"lastNameJp": "デイビス",
"firstNameJpKana": "ジェームズ",
"lastNameJpKana": "デイビス",
"email": "james.davis@email.com",
"mobile": "+61-2-9876-5433",
"originAdd1": "15 Collins Street, Melbourne, VIC 3000, Australia",
@@ -597,6 +662,10 @@
"custId": 4,
"firstNameEn": "Sophie",
"lastNameEn": "Davis",
"firstNameJp": "ソフィー",
"lastNameJp": "デイビス",
"firstNameJpKana": "ソフィー",
"lastNameJpKana": "デイビス",
"email": "sophie.davis@email.com",
"mobile": "+61-2-9876-5434",
"originAdd1": "15 Collins Street, Melbourne, VIC 3000, Australia",
@@ -607,6 +676,10 @@
"custId": 4,
"firstNameEn": "Oliver",
"lastNameEn": "Davis",
"firstNameJp": "オリバー",
"lastNameJp": "デイビス",
"firstNameJpKana": "オリバー",
"lastNameJpKana": "デイビス",
"email": "oliver.davis@email.com",
"mobile": "+61-2-9876-5435",
"originAdd1": "15 Collins Street, Melbourne, VIC 3000, Australia",
@@ -617,10 +690,42 @@
"custId": 5,
"firstNameEn": "Rachel",
"lastNameEn": "Wilson",
"firstNameJp": "レイチェル",
"lastNameJp": "ウィルソン",
"firstNameJpKana": "レイチェル",
"lastNameJpKana": "ウィルソン",
"email": "rachel.wilson@email.com",
"mobile": "+1-416-555-0191",
"originAdd1": "100 Queen Street West, Toronto, ON M5H 2N2, Canada",
"localAdd1": "987 Ginza District, Tokyo, Japan 104-0061"
},
{
"id": 1,
"custId": 1,
"firstNameEn": "Jane",
"lastNameEn": "Smith",
"firstNameJp": "ジェーン",
"lastNameJp": "スミス",
"firstNameJpKana": "ジェーン",
"lastNameJpKana": "スミス",
"email": "jane.smith@email.com",
"mobile": "+1-555-0111",
"originAdd1": "123 Main Street, New York, NY 10001, USA",
"localAdd1": "456 Oak Avenue, Tokyo, Japan 150-0001"
},
{
"id": 2,
"custId": 1,
"firstNameEn": "Tommy",
"lastNameEn": "Smith",
"firstNameJp": "トミー",
"lastNameJp": "スミス",
"firstNameJpKana": "トミー",
"lastNameJpKana": "スミス",
"email": "tommy.smith@email.com",
"mobile": "+1-555-0112",
"originAdd1": "123 Main Street, New York, NY 10001, USA",
"localAdd1": "456 Oak Avenue, Tokyo, Japan 150-0001"
}
]
}

View File

@@ -7,6 +7,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 1,
firstNameEn: "Jane",
lastNameEn: "Smith",
firstNameJp: "ジェーン",
lastNameJp: "スミス",
firstNameJpKana: "ジェーン",
lastNameJpKana: "スミス",
email: "jane.smith@email.com",
mobile: "+1-555-0111",
originAdd1: "123 Main Street, New York, NY 10001, USA",
@@ -17,6 +21,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 1,
firstNameEn: "Tommy",
lastNameEn: "Smith",
firstNameJp: "トミー",
lastNameJp: "スミス",
firstNameJpKana: "トミー",
lastNameJpKana: "スミス",
email: "tommy.smith@email.com",
mobile: "+1-555-0112",
originAdd1: "123 Main Street, New York, NY 10001, USA",
@@ -28,6 +36,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 2,
firstNameEn: "Mark",
lastNameEn: "Johnson",
firstNameJp: "マーク",
lastNameJp: "ジョンソン",
firstNameJpKana: "マーク",
lastNameJpKana: "ジョンソン",
email: "mark.johnson@email.com",
mobile: "+1-555-0121",
originAdd1: "789 Pine Road, Los Angeles, CA 90210, USA",
@@ -38,6 +50,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 2,
firstNameEn: "Lisa",
lastNameEn: "Johnson",
firstNameJp: "リサ",
lastNameJp: "ジョンソン",
firstNameJpKana: "リサ",
lastNameJpKana: "ジョンソン",
email: "lisa.johnson@email.com",
mobile: "+1-555-0122",
originAdd1: "789 Pine Road, Los Angeles, CA 90210, USA",
@@ -49,6 +65,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 3,
firstNameEn: "Anna",
lastNameEn: "Brown",
firstNameJp: "アンナ",
lastNameJp: "ブラウン",
firstNameJpKana: "アンナ",
lastNameJpKana: "ブラウン",
email: "anna.brown@email.com",
mobile: "+44-20-7946-0959",
originAdd1: "42 Victoria Street, London, UK SW1H 0TL",
@@ -60,6 +80,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 4,
firstNameEn: "James",
lastNameEn: "Davis",
firstNameJp: "ジェームズ",
lastNameJp: "デイビス",
firstNameJpKana: "ジェームズ",
lastNameJpKana: "デイビス",
email: "james.davis@email.com",
mobile: "+61-2-9876-5433",
originAdd1: "15 Collins Street, Melbourne, VIC 3000, Australia",
@@ -70,6 +94,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 4,
firstNameEn: "Sophie",
lastNameEn: "Davis",
firstNameJp: "ソフィー",
lastNameJp: "デイビス",
firstNameJpKana: "ソフィー",
lastNameJpKana: "デイビス",
email: "sophie.davis@email.com",
mobile: "+61-2-9876-5434",
originAdd1: "15 Collins Street, Melbourne, VIC 3000, Australia",
@@ -80,6 +108,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 4,
firstNameEn: "Oliver",
lastNameEn: "Davis",
firstNameJp: "オリバー",
lastNameJp: "デイビス",
firstNameJpKana: "オリバー",
lastNameJpKana: "デイビス",
email: "oliver.davis@email.com",
mobile: "+61-2-9876-5435",
originAdd1: "15 Collins Street, Melbourne, VIC 3000, Australia",
@@ -91,6 +123,10 @@ export const sampleCustomerDependants: CustomerDependant[] = [
custId: 5,
firstNameEn: "Rachel",
lastNameEn: "Wilson",
firstNameJp: "レイチェル",
lastNameJp: "ウィルソン",
firstNameJpKana: "レイチェル",
lastNameJpKana: "ウィルソン",
email: "rachel.wilson@email.com",
mobile: "+1-416-555-0191",
originAdd1: "100 Queen Street West, Toronto, ON M5H 2N2, Canada",

View File

@@ -5,45 +5,240 @@ export const sampleCustomers: Customer[] = [
id: 1,
firstNameEn: "John",
lastNameEn: "Smith",
firstNameJp: "ジョン",
lastNameJp: "スミス",
firstNameJpKana: "ジョン",
lastNameJpKana: "スミス",
email: "john.smith@email.com",
mobile: "+1-555-0101",
originAdd1: "123 Main Street, New York, NY 10001, USA",
localAdd1: "456 Oak Avenue, Tokyo, Japan 150-0001",
gender: "male",
martial: "married",
status: "active",
},
{
id: 2,
firstNameEn: "Sarah",
lastNameEn: "Johnson",
firstNameJp: "サラ",
lastNameJp: "ジョンソン",
firstNameJpKana: "サラ",
lastNameJpKana: "ジョンソン",
email: "sarah.johnson@email.com",
mobile: "+1-555-0102",
originAdd1: "789 Pine Road, Los Angeles, CA 90210, USA",
localAdd1: "321 Sakura Street, Osaka, Japan 530-0001",
gender: "female",
martial: "single",
status: "active",
},
{
id: 3,
firstNameEn: "Michael",
lastNameEn: "Brown",
firstNameJp: "マイケル",
lastNameJp: "ブラウン",
firstNameJpKana: "マイケル",
lastNameJpKana: "ブラウン",
email: "michael.brown@email.com",
mobile: "+44-20-7946-0958",
originAdd1: "42 Victoria Street, London, UK SW1H 0TL",
localAdd1: "789 Shibuya Crossing, Tokyo, Japan 150-0002",
gender: "male",
martial: "divorced",
status: "pending",
},
{
id: 4,
firstNameEn: "Emily",
lastNameEn: "Davis",
firstNameJp: "エミリー",
lastNameJp: "デイビス",
firstNameJpKana: "エミリー",
lastNameJpKana: "デイビス",
email: "emily.davis@email.com",
mobile: "+61-2-9876-5432",
originAdd1: "15 Collins Street, Melbourne, VIC 3000, Australia",
localAdd1: "654 Namba District, Osaka, Japan 542-0076",
gender: "female",
martial: "married",
status: "active",
},
{
id: 5,
firstNameEn: "David",
lastNameEn: "Wilson",
firstNameJp: "デイビッド",
lastNameJp: "ウィルソン",
firstNameJpKana: "デイビッド",
lastNameJpKana: "ウィルソン",
email: "david.wilson@email.com",
mobile: "+1-416-555-0199",
originAdd1: "100 Queen Street West, Toronto, ON M5H 2N2, Canada",
localAdd1: "987 Ginza District, Tokyo, Japan 104-0061",
gender: "male",
martial: "single",
status: "inactive",
},
{
id: 6,
firstNameEn: "Maria",
lastNameEn: "Garcia",
firstNameJp: "マリア",
lastNameJp: "ガルシア",
firstNameJpKana: "マリア",
lastNameJpKana: "ガルシア",
email: "maria.garcia@email.com",
mobile: "+34-91-555-0123",
originAdd1: "Calle Gran Vía 45, Madrid, Spain 28013",
localAdd1: "246 Harajuku Street, Tokyo, Japan 150-0001",
gender: "female",
martial: "married",
status: "active",
},
{
id: 7,
firstNameEn: "James",
lastNameEn: "Lee",
firstNameJp: "ジェームス",
lastNameJp: "リー",
firstNameJpKana: "ジェームス",
lastNameJpKana: "リー",
email: "james.lee@email.com",
mobile: "+65-6555-0187",
originAdd1: "1 Raffles Place, Singapore 048616",
localAdd1: "135 Roppongi Hills, Tokyo, Japan 106-6108",
gender: "male",
martial: "single",
status: "active",
},
{
id: 8,
firstNameEn: "Anna",
lastNameEn: "Mueller",
firstNameJp: "アンナ",
lastNameJp: "ミュラー",
firstNameJpKana: "アンナ",
lastNameJpKana: "ミュラー",
email: "anna.mueller@email.com",
mobile: "+49-30-555-0234",
originAdd1: "Unter den Linden 77, Berlin, Germany 10117",
localAdd1: "567 Shinjuku District, Tokyo, Japan 160-0022",
gender: "female",
martial: "widowed",
status: "pending",
},
{
id: 9,
firstNameEn: "Robert",
lastNameEn: "Taylor",
firstNameJp: "ロバート",
lastNameJp: "テイラー",
firstNameJpKana: "ロバート",
lastNameJpKana: "テイラー",
email: "robert.taylor@email.com",
mobile: "+1-555-0145",
originAdd1: "890 Broadway, New York, NY 10003, USA",
localAdd1: "890 Akasaka District, Tokyo, Japan 107-0052",
gender: "male",
martial: "married",
status: "inactive",
},
{
id: 10,
firstNameEn: "Lisa",
lastNameEn: "Anderson",
firstNameJp: "リサ",
lastNameJp: "アンダーソン",
firstNameJpKana: "リサ",
lastNameJpKana: "アンダーソン",
email: "lisa.anderson@email.com",
mobile: "+1-555-0167",
originAdd1: "456 Sunset Boulevard, Los Angeles, CA 90028, USA",
localAdd1: "234 Ikebukuro District, Tokyo, Japan 171-0021",
gender: "female",
martial: "divorced",
status: "active",
},
{
id: 11,
firstNameEn: "Alex",
lastNameEn: "Chen",
firstNameJp: "アレックス",
lastNameJp: "チェン",
firstNameJpKana: "アレックス",
lastNameJpKana: "チェン",
email: "alex.chen@email.com",
mobile: "+86-21-555-0189",
originAdd1: "88 Nanjing Road, Shanghai, China 200001",
localAdd1: "678 Aoyama District, Tokyo, Japan 107-0061",
gender: "other",
martial: "single",
status: "active",
},
{
id: 12,
firstNameEn: "Sophie",
lastNameEn: "Martin",
firstNameJp: "ソフィー",
lastNameJp: "マルタン",
firstNameJpKana: "ソフィー",
lastNameJpKana: "マルタン",
email: "sophie.martin@email.com",
mobile: "+33-1-555-0212",
originAdd1: "25 Champs-Élysées, Paris, France 75008",
localAdd1: "345 Ebisu District, Tokyo, Japan 150-0013",
gender: "female",
martial: "married",
status: "pending",
},
{
id: 13,
firstNameEn: "Thomas",
lastNameEn: "White",
firstNameJp: "トーマス",
lastNameJp: "ホワイト",
firstNameJpKana: "トーマス",
lastNameJpKana: "ホワイト",
email: "thomas.white@email.com",
mobile: "+1-555-0234",
originAdd1: "789 Michigan Avenue, Chicago, IL 60611, USA",
localAdd1: "456 Kanda District, Tokyo, Japan 101-0047",
gender: "male",
martial: "single",
status: "active",
},
{
id: 14,
firstNameEn: "Isabella",
lastNameEn: "Rodriguez",
firstNameJp: "イザベラ",
lastNameJp: "ロドリゲス",
firstNameJpKana: "イザベラ",
lastNameJpKana: "ロドリゲス",
email: "isabella.rodriguez@email.com",
mobile: "+52-55-555-0256",
originAdd1: "Avenida Reforma 123, Mexico City, Mexico 06600",
localAdd1: "789 Ueno District, Tokyo, Japan 110-0005",
gender: "female",
martial: "married",
status: "inactive",
},
{
id: 15,
firstNameEn: "William",
lastNameEn: "Thompson",
firstNameJp: "ウィリアム",
lastNameJp: "トンプソン",
firstNameJpKana: "ウィリアム",
lastNameJpKana: "トンプソン",
email: "william.thompson@email.com",
mobile: "+1-555-0278",
originAdd1: "567 Market Street, San Francisco, CA 94105, USA",
localAdd1: "123 Sumida District, Tokyo, Japan 130-0013",
gender: "male",
martial: "divorced",
status: "active",
},
];

View File

@@ -1,10 +1,11 @@
import { Permission } from "./database.schema";
export const samplePermissions: Permission[] = [
// User Management Permissions
{
id: 1,
name: "user_read",
description: "Read user information",
description: "View user list and user details",
isActive: true,
},
{
@@ -21,74 +22,36 @@ export const samplePermissions: Permission[] = [
},
{
id: 4,
name: "user_permissions",
description: "Manage user permissions",
isActive: true,
},
// Customer Management Permissions
{
id: 5,
name: "customer_read",
description: "Read customer information",
description: "View customer list and customer details",
isActive: true,
},
{
id: 5,
id: 6,
name: "customer_write",
description: "Create and update customer information",
isActive: true,
},
{
id: 6,
id: 7,
name: "customer_delete",
description: "Delete customer records",
isActive: true,
},
{
id: 7,
name: "admin_panel",
description: "Access administrative panel",
isActive: true,
},
// Mail Template Permissions
{
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",
name: "mail_template_access",
description: "Access mail template configuration",
isActive: true,
},
];

View File

@@ -2,82 +2,62 @@ 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 },
{ id: 1, userId: 1, permissionId: 1 }, // user_read
{ id: 2, userId: 1, permissionId: 2 }, // user_write
{ id: 3, userId: 1, permissionId: 3 }, // user_delete
{ id: 4, userId: 1, permissionId: 4 }, // user_permissions
{ id: 5, userId: 1, permissionId: 5 }, // customer_read
{ id: 6, userId: 1, permissionId: 6 }, // customer_write
{ id: 7, userId: 1, permissionId: 7 }, // customer_delete
{ id: 8, userId: 1, permissionId: 8 }, // mail_template_access
// 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
{ id: 9, userId: 2, permissionId: 5 }, // customer_read
{ id: 10, userId: 2, permissionId: 6 }, // customer_write
{ id: 11, userId: 2, permissionId: 8 }, // mail_template_access
// 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
{ id: 12, userId: 3, permissionId: 1 }, // user_read
{ id: 13, userId: 3, permissionId: 2 }, // user_write
{ id: 14, userId: 3, permissionId: 4 }, // user_permissions
// 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
// Mike Johnson (id: 4) - Customer Support Lead
{ id: 15, userId: 4, permissionId: 5 }, // customer_read
{ id: 16, userId: 4, permissionId: 6 }, // customer_write
{ id: 17, userId: 4, permissionId: 7 }, // customer_delete
{ id: 18, userId: 4, permissionId: 1 }, // user_read
// 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
{ id: 19, userId: 5, permissionId: 5 }, // customer_read
{ id: 20, userId: 5, permissionId: 6 }, // customer_write
{ id: 21, userId: 5, permissionId: 8 }, // mail_template_access
// 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
// David Brown (id: 6) - HR Manager
{ id: 22, userId: 6, permissionId: 1 }, // user_read
{ id: 23, userId: 6, permissionId: 2 }, // user_write
{ id: 24, userId: 6, permissionId: 4 }, // user_permissions
{ id: 25, userId: 6, permissionId: 8 }, // mail_template_access
// 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
// Emma Davis (id: 7) - Mail Template Manager
{ id: 26, userId: 7, permissionId: 8 }, // mail_template_access
{ id: 27, userId: 7, permissionId: 5 }, // 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
// Alex Martinez (id: 8) - Junior Support
{ id: 28, userId: 8, permissionId: 5 }, // customer_read
{ id: 29, userId: 8, permissionId: 1 }, // user_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
// Lisa Garcia (id: 9) - Senior Customer Manager
{ id: 30, userId: 9, permissionId: 5 }, // customer_read
{ id: 31, userId: 9, permissionId: 6 }, // customer_write
{ id: 32, userId: 9, permissionId: 7 }, // customer_delete
{ id: 33, userId: 9, permissionId: 8 }, // mail_template_access
// 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
// Robert Taylor (id: 10) - Read-only User
{ id: 34, userId: 10, permissionId: 5 }, // customer_read
{ id: 35, userId: 10, permissionId: 1 }, // user_read
// 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
{ id: 36, userId: 11, permissionId: 5 }, // customer_read
{ id: 37, userId: 11, permissionId: 6 }, // customer_write
{ id: 38, userId: 11, permissionId: 8 }, // mail_template_access
];

View File

@@ -3,25 +3,36 @@ import { z } from "zod";
export const customerInfoSchema = z.object({
firstNameEn: z.string().min(1, "First name is required"),
lastNameEn: z.string().min(1, "Last name is required"),
originAdd1: z.string().min(1, "Origin address is required"),
localAdd1: z.string().min(1, "Local address is required"),
firstNameJp: z.string().min(1, "First name in Japanese is required"),
lastNameJp: z.string().min(1, "Last name in Japanese is required"),
firstNameJpKana: z.string().min(1, "First name in Japanese Kana is required"),
lastNameJpKana: z.string().min(1, "Last name in Japanese Kana is required"),
gender: z.string().min(1, "Gender is required"),
martial: z.string().min(1, "Marital status is required"),
status: z.string().min(1, "Status is required"),
});
export const customerContactSchema = z.object({
email: z.string().email("Invalid email address"),
mobile: z.string().min(1, "Mobile number is required"),
originAdd1: z.string().min(1, "Origin address is required"),
localAdd1: z.string().min(1, "Local address is required"),
});
export const customerDependantInfoSchema = z.object({
firstNameEn: z.string().min(1, "First name is required"),
lastNameEn: z.string().min(1, "Last name is required"),
originAdd1: z.string().min(1, "Origin address is required"),
localAdd1: z.string().min(1, "Local address is required"),
firstNameJp: z.string().min(1, "First name in Japanese is required"),
lastNameJp: z.string().min(1, "Last name in Japanese is required"),
firstNameJpKana: z.string().min(1, "First name in Japanese Kana is required"),
lastNameJpKana: z.string().min(1, "Last name in Japanese Kana is required"),
});
export const customerDependantContactSchema = z.object({
email: z.string().email("Invalid email address"),
mobile: z.string().min(1, "Mobile number is required"),
originAdd1: z.string().min(1, "Origin address is required"),
localAdd1: z.string().min(1, "Local address is required"),
});
export const customerDependantFormSchema = z.object({