Browse Source

created the report management Jasper report1 as sample

master
[email protected] 3 weeks ago
parent
commit
5daf8c1f79
5 changed files with 252 additions and 15 deletions
  1. +167
    -0
      src/app/(main)/report/page.tsx
  2. +7
    -0
      src/components/NavigationContent/NavigationContent.tsx
  3. +51
    -0
      src/config/reportConfig.ts
  4. +24
    -15
      src/middleware.ts
  5. +3
    -0
      src/routes.ts

+ 167
- 0
src/app/(main)/report/page.tsx View File

@@ -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<string>('');
const [criteria, setCriteria] = useState<Record<string, string>>({});
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<HTMLInputElement>) => {
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 (
<Box sx={{ p: 4, maxWidth: 1000, margin: '0 auto' }}>
<Typography variant="h4" gutterBottom fontWeight="bold">
Report Management
</Typography>
<Card sx={{ mb: 4, boxShadow: 3 }}>
<CardContent>
<Typography variant="h6" gutterBottom>
Select Report Type
</Typography>
<TextField
select
fullWidth
label="Report List"
value={selectedReportId}
onChange={handleReportChange}
helperText="Please select which report you want to generate"
>
{REPORTS.map((report) => (
<MenuItem key={report.id} value={report.id}>
{report.title}
</MenuItem>
))}
</TextField>
</CardContent>
</Card>

{currentReport && (
<Card sx={{ boxShadow: 3, animation: 'fadeIn 0.5s' }}>
<CardContent>
<Typography variant="h6" color="primary" gutterBottom>
Search Criteria: {currentReport.title}
</Typography>
<Divider sx={{ mb: 3 }} />
<Grid container spacing={3}>
{currentReport.fields.map((field) => (
<Grid item xs={12} sm={6} key={field.name}>
<TextField
fullWidth
label={field.label}
type={field.type}
placeholder={field.placeholder}
InputLabelProps={field.type === 'date' ? { shrink: true } : {}}
onChange={(e) => handleFieldChange(field.name, e.target.value)}
value={criteria[field.name] || ''}
select={field.type === 'select'}
>
{field.type === 'select' && field.options?.map((opt) => (
<MenuItem key={opt.value} value={opt.value}>
{opt.label}
</MenuItem>
))}
</TextField>
</Grid>
))}
</Grid>

<Box sx={{ mt: 4, display: 'flex', justifyContent: 'flex-end' }}>
<Button
variant="contained"
size="large"
startIcon={<PrintIcon />}
onClick={handlePrint}
disabled={loading}
sx={{ px: 4 }}
>
{loading ? "Generating..." : "Print Report"}
</Button>
</Box>
</CardContent>
</Card>
)}
</Box>
);
}

+ 7
- 0
src/components/NavigationContent/NavigationContent.tsx View File

@@ -248,6 +248,13 @@ const NavigationContent: React.FC = () => {
requiredAbility: TESTING,
isHidden: false,
},
{
icon: <BugReportIcon />,
label: "Report Management",
path: "/report",
requiredAbility: TESTING,
isHidden: false,
},
{
icon: <RequestQuote />,
label: "Settings",


+ 51
- 0
src/config/reportConfig.ts View File

@@ -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...
];

+ 24
- 15
src/middleware.ts View File

@@ -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
}

+ 3
- 0
src/routes.ts View File

@@ -2,6 +2,9 @@ export const PRIVATE_ROUTES = [
"/analytics",
"/dashboard",
"/dashboard",
"/testing",
"/ps",
"/report",
"/invoice",
"/projects",
"/tasks",


Loading…
Cancel
Save