From 5daf8c1f790b25017ae983341811102eed22bb4f Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Wed, 14 Jan 2026 00:28:00 +0800 Subject: [PATCH] created the report management Jasper report1 as sample --- src/app/(main)/report/page.tsx | 167 ++++++++++++++++++ .../NavigationContent/NavigationContent.tsx | 7 + src/config/reportConfig.ts | 51 ++++++ src/middleware.ts | 39 ++-- src/routes.ts | 3 + 5 files changed, 252 insertions(+), 15 deletions(-) create mode 100644 src/app/(main)/report/page.tsx create mode 100644 src/config/reportConfig.ts diff --git a/src/app/(main)/report/page.tsx b/src/app/(main)/report/page.tsx new file mode 100644 index 0000000..c1b3edb --- /dev/null +++ b/src/app/(main)/report/page.tsx @@ -0,0 +1,167 @@ +"use client"; + +import React, { useState, useMemo } from 'react'; +import { + Box, + Card, + CardContent, + Typography, + MenuItem, + TextField, + Button, + Grid, + Divider +} from '@mui/material'; +import PrintIcon from '@mui/icons-material/Print'; +import { REPORTS, ReportDefinition } from '@/config/reportConfig'; +import { getSession } from "next-auth/react"; + +export default function ReportPage() { + const [selectedReportId, setSelectedReportId] = useState(''); + const [criteria, setCriteria] = useState>({}); + const [loading, setLoading] = useState(false); + + // Find the configuration for the currently selected report + const currentReport = useMemo(() => + REPORTS.find((r) => r.id === selectedReportId), + [selectedReportId]); + + const handleReportChange = (event: React.ChangeEvent) => { + setSelectedReportId(event.target.value); + setCriteria({}); // Clear criteria when switching reports + }; + + const handleFieldChange = (name: string, value: string) => { + setCriteria((prev) => ({ ...prev, [name]: value })); + }; + + const handlePrint = async () => { + if (!currentReport) return; + + // 1. Mandatory Field Validation + const missingFields = currentReport.fields + .filter(field => field.required && !criteria[field.name]) + .map(field => field.label); + + if (missingFields.length > 0) { + alert(`Please enter the following mandatory fields:\n- ${missingFields.join('\n- ')}`); + return; + } + + setLoading(true); + try { + const token = localStorage.getItem("accessToken"); + const queryParams = new URLSearchParams(criteria).toString(); + const url = `${currentReport.apiEndpoint}?${queryParams}`; + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/pdf', + }, + }); + + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); + + const blob = await response.blob(); + const downloadUrl = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = downloadUrl; + + const contentDisposition = response.headers.get('Content-Disposition'); + let fileName = `${currentReport.title}.pdf`; + if (contentDisposition?.includes('filename=')) { + fileName = contentDisposition.split('filename=')[1].split(';')[0].replace(/"/g, ''); + } + + link.setAttribute('download', fileName); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(downloadUrl); + } catch (error) { + console.error("Failed to generate report:", error); + alert("An error occurred while generating the report. Please try again."); + } finally { + setLoading(false); + } + }; + + return ( + + + Report Management + + + + + + Select Report Type + + + {REPORTS.map((report) => ( + + {report.title} + + ))} + + + + + {currentReport && ( + + + + Search Criteria: {currentReport.title} + + + + + {currentReport.fields.map((field) => ( + + handleFieldChange(field.name, e.target.value)} + value={criteria[field.name] || ''} + select={field.type === 'select'} + > + {field.type === 'select' && field.options?.map((opt) => ( + + {opt.label} + + ))} + + + ))} + + + + + + + + )} + + ); +} \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 252b803..5448a6d 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -248,6 +248,13 @@ const NavigationContent: React.FC = () => { requiredAbility: TESTING, isHidden: false, }, + { + icon: , + label: "Report Management", + path: "/report", + requiredAbility: TESTING, + isHidden: false, + }, { icon: , label: "Settings", diff --git a/src/config/reportConfig.ts b/src/config/reportConfig.ts new file mode 100644 index 0000000..9381927 --- /dev/null +++ b/src/config/reportConfig.ts @@ -0,0 +1,51 @@ +export type FieldType = 'date' | 'text' | 'select' | 'number'; + +import { NEXT_PUBLIC_API_URL } from "@/config/api"; + +export interface ReportField { + label: string; + name: string; + type: FieldType; + placeholder?: string; + required: boolean; + options?: { label: string; value: string }[]; // For select types +} + +export interface ReportDefinition { + id: string; + title: string; + apiEndpoint: string; + fields: ReportField[]; +} + +export const REPORTS: ReportDefinition[] = [ + { + id: "rep-001", + title: "Report 1", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-report1`, + fields: [ + { label: "From Date", name: "fromDate", type: "date", required: true }, // Mandatory + { label: "To Date", name: "toDate", type: "date", required: true }, // Mandatory + { label: "Item Code", name: "itemCode", type: "text", required: false, placeholder: "e.g. FG"}, + { label: "Item Type", name: "itemType", type: "select", required: false, + options: [ + { label: "FG", value: "FG" }, + { label: "Material", value: "Mat" } + ] }, + ] + }, + { + id: "rep-002", + title: "Report 2", + apiEndpoint: `${NEXT_PUBLIC_API_URL}/report/print-report2`, + fields: [ + { label: "Target Date", name: "targetDate", type: "date", required: false }, + { label: "Item Code", name: "itemCode", type: "text", required: false }, + { label: "Shift", name: "shift", type: "select", options: [ + { label: "Day", value: "D" }, + { label: "Night", value: "N" } + ], required: false} + ] + }, + // Add Report 3 to 10 following the same pattern... +]; \ No newline at end of file diff --git a/src/middleware.ts b/src/middleware.ts index 5bfb8cd..d71df88 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,5 +1,4 @@ import { NextRequestWithAuth, withAuth } from "next-auth/middleware"; -// import { authOptions } from "@/config/authConfig"; import { authOptions } from "./config/authConfig"; import { NextFetchEvent, NextResponse } from "next/server"; import { PRIVATE_ROUTES } from "./routes"; @@ -10,15 +9,14 @@ const authMiddleware = withAuth({ pages: authOptions.pages, callbacks: { authorized: ({ req, token }) => { - if (!Boolean(token)) { - return Boolean(token); + const currentTime = Math.floor(Date.now() / 1000); + + // Redirect to login if: + // 1. No token exists + // 2. Token has an expiry field (exp) and current time has passed it + if (!token || (token.exp && currentTime > (token.exp as number))) { + return false; } - - // example - // const abilities = token!.abilities as string[] - // if (req.nextUrl.pathname.endsWith('/user') && 'abilities dont hv view/maintain user') { - // return false - // } return true; }, }, @@ -28,9 +26,9 @@ export default async function middleware( req: NextRequestWithAuth, event: NextFetchEvent, ) { + // Handle language parameters const langPref = req.nextUrl.searchParams.get(LANG_QUERY_PARAM); if (langPref) { - // Redirect to same url without the lang query param + set cookies const newUrl = new URL(req.nextUrl); newUrl.searchParams.delete(LANG_QUERY_PARAM); const response = NextResponse.redirect(newUrl); @@ -38,8 +36,19 @@ export default async function middleware( return response; } - // Matcher for using the auth middleware - return PRIVATE_ROUTES.some((route) => req.nextUrl.pathname.startsWith(route)) - ? await authMiddleware(req, event) // Let auth middleware handle response - : NextResponse.next(); // Return normal response -} + // Check if the current URL starts with any string in PRIVATE_ROUTES + const isPrivateRoute = PRIVATE_ROUTES.some((route) => + req.nextUrl.pathname.startsWith(route) + ); + + // Debugging: View terminal logs to see if the path is being caught + if (req.nextUrl.pathname.startsWith("/ps") || req.nextUrl.pathname.startsWith("/testing")) { + console.log("--- Middleware Check ---"); + console.log("Path:", req.nextUrl.pathname); + console.log("Is Private Match:", isPrivateRoute); + } + + return isPrivateRoute + ? await authMiddleware(req, event) // Run authentication check + : NextResponse.next(); // Allow public access +} \ No newline at end of file diff --git a/src/routes.ts b/src/routes.ts index 0c58071..bff37ed 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -2,6 +2,9 @@ export const PRIVATE_ROUTES = [ "/analytics", "/dashboard", "/dashboard", + "/testing", + "/ps", + "/report", "/invoice", "/projects", "/tasks",