|
- "use client";
-
- import React, { useState, useMemo, useEffect, useRef } from 'react';
- import { useSession } from "next-auth/react";
- import { SessionWithTokens } from "@/config/authConfig";
- import { AUTH } from "@/authorities";
- import {
- Box,
- Card,
- CardContent,
- Typography,
- MenuItem,
- TextField,
- Button,
- Grid,
- Divider,
- Chip,
- Autocomplete,
- Checkbox,
- FormControlLabel,
- Dialog,
- DialogTitle,
- DialogContent,
- DialogActions,
- } from '@mui/material';
- import DownloadIcon from '@mui/icons-material/Download';
- import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
- import { REPORTS } from '@/config/reportConfig';
- import { NEXT_PUBLIC_API_URL } from '@/config/api';
- import { clientAuthFetch } from '@/app/utils/clientAuthFetch';
- import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport';
- import AsyncItemCodeAutocomplete from './AsyncItemCodeAutocomplete';
- import ReportSelectionDashboard from './ReportSelectionDashboard';
- import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers';
- import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
- import dayjs from 'dayjs';
- import 'dayjs/locale/zh-hk';
- import { OUTPUT_DATE_FORMAT } from '@/app/utils/formatUtil';
- import { useReportLabels } from './reportI18n';
- import {
- fetchSemiFGItemCodes,
- fetchSemiFGItemCodesWithCategory
- } from './semiFGProductionAnalysisApi';
- import { generateGrnReportExcel } from './grnReportApi';
- import { generateBomShopSyncReportExcel } from './bomShopSyncReportApi';
- import { generateShopOrderReplenishmentReportExcel } from './shopOrderReplenishmentReportApi';
- import {
- FEATURE_USAGE,
- FEATURE_USAGE_ACTION,
- logFeatureUsage,
- } from '@/lib/featureUsageLog';
- import { error as errorColor } from '@/theme/devias-material-kit/colors';
-
- interface ItemCodeWithName {
- code: string;
- name: string;
- }
-
- const FIELD_ERROR_SX = {
- '& .MuiOutlinedInput-root.Mui-error': {
- '& .MuiOutlinedInput-notchedOutline': {
- borderColor: 'error.dark',
- boxShadow: `0 0 0 2px ${errorColor.dark}40`,
- },
- },
- '& .MuiInputLabel-root.Mui-error': {
- color: 'error.dark',
- },
- '& .MuiFormHelperText-root.Mui-error': {
- color: 'error.dark',
- },
- '& .MuiInputLabel-asterisk': {
- color: 'error.dark',
- },
- };
-
- /** FP-MTMS Version Checklist | Functions Ref. No. 39 | v1.0.3 | 2026-09-10 */
- /** FP-MTMS Version Checklist | Functions Ref. No. 60 | v1.0.1 | 2026-08-11 */
- /** FP-MTMS Version Checklist | Functions Ref. No. 69 | v1.0.2 | 2026-09-10 */
- /** FP-MTMS Version Checklist | Functions Ref. No. 82 | v1.0.0 | 2026-09-10 */
- export default function ReportPage() {
- const { data: session } = useSession() as { data: SessionWithTokens | null };
- const { t, i18n, reportTitle, fieldLabel, optionLabel } = useReportLabels();
- const isZh = (i18n.language || 'zh').startsWith('zh');
- const dateDisplayFormat = isZh ? 'DD/MM/YYYY' : 'DD/MM/YYYY';
- const includeGrnFinancialColumns =
- session?.abilities?.includes(AUTH.ADMIN) ?? false;
-
- const [selectedReportId, setSelectedReportId] = useState<string>('');
- const [criteria, setCriteria] = useState<Record<string, string>>({});
- const [loading, setLoading] = useState(false);
- const excelInFlightRef = useRef(false);
- const [dynamicOptions, setDynamicOptions] = useState<Record<string, { label: string; value: string }[]>>({});
- const [showConfirmDialog, setShowConfirmDialog] = useState(false);
- const [showNoDataDialog, setShowNoDataDialog] = useState(false);
- const [fieldErrors, setFieldErrors] = useState<Record<string, string>>({});
-
- // Find the configuration for the currently selected report
- const rep012RoundIds = useMemo(() => {
- if (selectedReportId !== 'rep-012') return [] as string[];
- return (criteria.stockTakeRoundId || '')
- .split(',')
- .map((s) => s.trim())
- .filter(Boolean);
- }, [selectedReportId, criteria.stockTakeRoundId]);
-
- const rep012MultiRound = rep012RoundIds.length > 1;
-
- const currentReport = useMemo(() =>
- REPORTS.find((r) => r.id === selectedReportId),
- [selectedReportId]);
-
- const handleSelectReport = (reportId: string) => {
- if (reportId === selectedReportId) return;
- setSelectedReportId(reportId);
- setFieldErrors({});
- if (reportId === 'rep-010') {
- setCriteria({ qcType: 'all', qcItemScope: 'all' });
- } else if (reportId === 'rep-004') {
- setCriteria({ storeId: 'All', poPrefix: 'All' });
- } else if (reportId === 'rep-021') {
- setCriteria({ storeId: 'All', stockTakeSectionDescription: 'All', lotOrigin: 'All' });
- } else {
- setCriteria({});
- }
- };
-
- const handleFieldChange = (name: string, value: string | string[]) => {
- const stringValue = Array.isArray(value) ? value.join(',') : value;
- setFieldErrors((prev) => {
- if (!prev[name]) return prev;
- const next = { ...prev };
- delete next[name];
- return next;
- });
- setCriteria((prev) => {
- const next = { ...prev, [name]: stringValue };
- if (currentReport?.id === 'rep-021' && name === 'warehouse') {
- const m = stringValue.trim().match(/^w(\d)/i);
- if (m) next.storeId = `${m[1]}F`;
- }
- return next;
- });
-
- // If this is stockCategory and there's a field that depends on it, fetch dynamic options
- if (name === 'stockCategory' && currentReport) {
- const itemCodeField = currentReport.fields.find(f => f.name === 'itemCode' && f.dynamicOptions);
- if (itemCodeField && itemCodeField.dynamicOptionsEndpoint) {
- fetchDynamicOptions(itemCodeField, stringValue);
- }
- }
- };
-
- const fetchDynamicOptions = async (field: any, paramValue: string) => {
- if (!field.dynamicOptionsEndpoint) return;
-
- try {
- // Use API service for SemiFG Production Analysis Report (rep-005)
- if (currentReport?.id === 'rep-005' && field.name === 'itemCode') {
- const itemCodesWithName = await fetchSemiFGItemCodes(paramValue);
- const itemsWithCategory = await fetchSemiFGItemCodesWithCategory(paramValue);
-
- const categoryMap: Record<string, { code: string; category: string; name?: string }> = {};
- itemsWithCategory.forEach(item => {
- categoryMap[item.code] = item;
- });
-
- const options = itemCodesWithName.map(item => {
- const code = item.code;
- const name = item.name || '';
- const category = categoryMap[code]?.category || '';
-
- let label = name ? `${code} ${name}` : code;
- if (category) {
- label = `${label} (${category})`;
- }
-
- return { label, value: code };
- });
-
- setDynamicOptions((prev) => ({ ...prev, [field.name]: options }));
- return;
- }
-
- // Handle other reports with dynamic options
- let url = field.dynamicOptionsEndpoint;
- if (paramValue && paramValue !== 'All' && !paramValue.includes('All')) {
- url = `${field.dynamicOptionsEndpoint}?${field.dynamicOptionsParam}=${paramValue}`;
- }
-
- const response = await clientAuthFetch(url, {
- method: 'GET',
- headers: { 'Content-Type': 'application/json' },
- });
-
- if (response.status === 401 || response.status === 403) return;
- if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
-
- const data = await response.json();
- const options = Array.isArray(data)
- ? field.name === 'stockTakeSectionDescription'
- ? (() => {
- const seen = new Set<string>();
- const mapped: { label: string; value: string }[] = [{ label: '全部', value: 'All' }];
- data.forEach((item: { stockTakeSectionDescription?: string; stockTakeSection?: string }) => {
- const desc = (item.stockTakeSectionDescription || '').trim();
- if (!desc || seen.has(desc)) return;
- seen.add(desc);
- const section = (item.stockTakeSection || '').trim();
- mapped.push({
- label: section ? `${desc} (${section})` : desc,
- value: desc,
- });
- });
- return mapped;
- })()
- : data.map((item: any) => ({
- label: item.label || item.name || item.code || String(item),
- value: item.value || item.code || String(item),
- }))
- : [];
-
- setDynamicOptions((prev) => ({ ...prev, [field.name]: options }));
- } catch (error) {
- console.error("Failed to fetch dynamic options:", error);
- setDynamicOptions((prev) => ({ ...prev, [field.name]: [] }));
- }
- };
-
- // Load initial options when report is selected
- useEffect(() => {
- if (currentReport) {
- currentReport.fields.forEach(field => {
- if (field.dynamicOptions && field.dynamicOptionsEndpoint) {
- // Load all options initially
- fetchDynamicOptions(field, '');
- }
- });
- }
- // Clear dynamic options when report changes
- setDynamicOptions({});
-
- // Default "All" (no filter) for stock take variance report conditions.
- if (selectedReportId === 'rep-012') {
- setCriteria({
- store_id: 'All',
- status: 'All',
- type: 'All',
- });
- }
- }, [selectedReportId]);
-
- /** rep-012:多選輪次時狀態固定為已審核 */
- useEffect(() => {
- if (selectedReportId !== 'rep-012' || !rep012MultiRound) return;
- if (criteria.status === 'completed') return;
- setCriteria((prev) => ({ ...prev, status: 'completed' }));
- }, [selectedReportId, rep012MultiRound, criteria.status]);
-
- // React 18 Strict Mode (dev) mounts → unmounts → remounts, so effects with [] run twice.
- // Dedupe PAGE_VIEW within a short window so 進入頁面次數 is +1 per real visit.
- useEffect(() => {
- if (typeof window === "undefined") return;
- const w = window as Window & { __fpsmsReportPageViewLoggedAt?: number };
- const now = Date.now();
- if (w.__fpsmsReportPageViewLoggedAt != null && now - w.__fpsmsReportPageViewLoggedAt < 2000) {
- return;
- }
- w.__fpsmsReportPageViewLoggedAt = now;
- logFeatureUsage(FEATURE_USAGE.REPORT_MANAGEMENT, FEATURE_USAGE_ACTION.PAGE_VIEW);
- }, []);
-
- const validateRequiredFields = () => {
- if (!currentReport) return true;
-
- if (currentReport.id === 'rep-012') {
- if (rep012RoundIds.length === 0) {
- setFieldErrors({ stockTakeRoundId: t('requiredField') });
- return false;
- }
- setFieldErrors({});
- return true;
- }
-
- const missingFields = currentReport.fields.filter((field) => {
- if (!field.required) return false;
- return !criteria[field.name];
- });
-
- if (missingFields.length > 0) {
- const nextErrors: Record<string, string> = {};
- missingFields.forEach((field) => {
- nextErrors[field.name] = t('requiredField');
- });
- setFieldErrors(nextErrors);
- return false;
- }
-
- setFieldErrors({});
-
- // Date fields with minDate: 'today' must not be before local today
- const today = new Date();
- const todayStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
- const beforeToday = currentReport.fields
- .filter((field) => field.type === 'date' && field.minDate === 'today')
- .filter((field) => {
- const v = (criteria[field.name] || '').trim();
- return v && v < todayStr;
- })
- .map((field) => fieldLabel(currentReport.id, field));
-
- if (beforeToday.length > 0) {
- alert(t('dateNotBeforeToday', { fields: beforeToday.join('\n- ') }));
- return false;
- }
-
- return true;
- };
-
- /** rep-012:單輪送 status;多輪送 stockTakeRoundId 清單且 status=completed */
- const buildRep012QueryString = (): string => {
- const p = new URLSearchParams();
- p.set('stockTakeRoundId', rep012RoundIds.join(','));
- const code = criteria.itemCode?.trim();
- if (code) p.set('itemCode', code);
- const store = criteria.store_id?.trim();
- if (store && store !== 'All') p.set('store_id', store);
- if (rep012MultiRound) {
- p.set('status', 'completed');
- } else {
- const status = criteria.status?.trim();
- if (status && status !== 'All') p.set('status', status);
- }
- const lotType = criteria.type?.trim();
- if (lotType && lotType !== 'All') p.set('type', lotType);
- return p.toString();
- };
-
- /** FP-MTMS Version Checklist | Functions Ref. No. 64 | v1.0.0 | 2026-08-11 */
- /** rep-010:qcItemScope → includeMeasurable / includeOther;qcType=all 不傳篩選 */
- const buildRep010QueryString = (): string => {
- const p = new URLSearchParams();
- Object.entries(criteria).forEach(([key, value]) => {
- if (key === 'qcItemScope') return;
- if (key === 'qcType' && String(value).trim().toLowerCase() === 'all') return;
- if (value != null && String(value).trim() !== '') {
- p.set(key, String(value));
- }
- });
- const scope = (criteria.qcItemScope || 'all').trim().toLowerCase();
- if (scope === 'measurable') {
- p.set('includeMeasurable', 'true');
- p.set('includeOther', 'false');
- } else {
- p.set('includeMeasurable', 'true');
- p.set('includeOther', 'true');
- }
- return p.toString();
- };
-
- const handlePrint = async () => {
- if (!currentReport) return;
- if (!validateRequiredFields()) return;
-
- // For rep-005, the print logic is handled by SemiFGProductionAnalysisReport component
- if (currentReport.id === 'rep-005') return;
-
- // For Excel reports (e.g. GRN), fetch JSON and download as .xlsx
- if (currentReport.responseType === 'excel') {
- await executeExcelReport();
- return;
- }
-
- await executePrint();
- };
-
- const handleExcelPrint = async () => {
- if (!currentReport) return;
- if (!validateRequiredFields()) return;
- await executeExcelReport();
- };
-
- const executeExcelReport = async () => {
- if (!currentReport) return;
- if (excelInFlightRef.current) return;
- excelInFlightRef.current = true;
- setLoading(true);
- try {
- if (currentReport.id === 'rep-014') {
- await generateGrnReportExcel(
- criteria,
- reportTitle(currentReport),
- includeGrnFinancialColumns,
- t,
- );
- } else if (currentReport.id === 'rep-015') {
- await generateBomShopSyncReportExcel(criteria, reportTitle(currentReport), t);
- } else if (currentReport.id === 'rep-017') {
- await generateShopOrderReplenishmentReportExcel(criteria, reportTitle(currentReport), t);
- } else {
- // Backend returns actual .xlsx bytes for this Excel endpoint.
- let queryParams =
- currentReport.id === 'rep-012'
- ? buildRep012QueryString()
- : currentReport.id === 'rep-010'
- ? buildRep010QueryString()
- : new URLSearchParams(criteria).toString();
- // rep-016: single-day UI — mirror dateStart to dateEnd for backend API.
- if (currentReport.id === 'rep-016') {
- const p = new URLSearchParams(criteria);
- const day = (criteria.dateStart || '').trim();
- if (day) {
- p.set('dateStart', day);
- p.set('dateEnd', day);
- }
- queryParams = p.toString();
- }
- const excelUrl = `${currentReport.apiEndpoint}-excel?${queryParams}`;
-
- const response = await clientAuthFetch(excelUrl, {
- method: 'GET',
- headers: { Accept: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
- });
-
- if (response.status === 401 || response.status === 403) return;
- if (response.status === 204) {
- setShowNoDataDialog(true);
- return;
- }
- if (!response.ok) {
- const errorText = await response.text();
- console.error("Response error:", errorText);
- throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
- }
-
- 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 = `${reportTitle(currentReport)}.xlsx`;
- 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);
- }
- if (currentReport) {
- logFeatureUsage(
- FEATURE_USAGE.REPORT_MANAGEMENT,
- FEATURE_USAGE_ACTION.DOWNLOAD,
- `${currentReport.id}:excel`,
- );
- }
- setShowConfirmDialog(false);
- } catch (error) {
- console.error("Failed to generate Excel report:", error);
- alert(t('generateError'));
- } finally {
- setLoading(false);
- excelInFlightRef.current = false;
- }
- };
-
- const executePrint = async () => {
- if (!currentReport) return;
-
- setLoading(true);
- try {
- let queryParams =
- currentReport.id === 'rep-012'
- ? buildRep012QueryString()
- : currentReport.id === 'rep-010'
- ? buildRep010QueryString()
- : new URLSearchParams(criteria).toString();
- const url = `${currentReport.apiEndpoint}?${queryParams}`;
-
- const response = await clientAuthFetch(url, {
- method: 'GET',
- headers: { 'Accept': 'application/pdf' },
- });
-
- if (response.status === 401 || response.status === 403) return;
- if (!response.ok) {
- const errorText = await response.text();
- console.error("Response error:", errorText);
- throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`);
- }
-
- 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 = `${reportTitle(currentReport)}.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);
-
- logFeatureUsage(
- FEATURE_USAGE.REPORT_MANAGEMENT,
- FEATURE_USAGE_ACTION.DOWNLOAD,
- `${currentReport.id}:pdf`,
- );
-
- setShowConfirmDialog(false);
- } catch (error) {
- console.error("Failed to generate report:", error);
- alert(t('generateError'));
- } finally {
- setLoading(false);
- }
- };
-
- return (
- <>
- <Box sx={{ p: 4, maxWidth: 1280, margin: '0 auto' }}>
- <Typography variant="h4" gutterBottom fontWeight="bold">
- {t('title')}
- </Typography>
-
- <ReportSelectionDashboard
- selectedReportId={selectedReportId}
- onSelectReport={handleSelectReport}
- />
-
- {currentReport && (
- <Card sx={{ boxShadow: 3, animation: 'fadeIn 0.5s' }}>
- <CardContent>
- <Typography variant="h6" color="primary" gutterBottom>
- {t('searchCriteriaWithTitle', { title: reportTitle(currentReport) })}
- </Typography>
- <Divider sx={{ mb: 3 }} />
-
- <LocalizationProvider
- dateAdapter={AdapterDayjs}
- adapterLocale={isZh ? 'zh-hk' : 'en'}
- localeText={
- isZh
- ? {
- fieldDayPlaceholder: () => '日',
- fieldMonthPlaceholder: () => '月',
- fieldYearPlaceholder: () => '年',
- }
- : undefined
- }
- >
- <Grid container spacing={3}>
- {currentReport.fields.map((field) => {
- const fieldKey = `${currentReport.id}-${field.name}`;
- const translatedLabel = fieldLabel(currentReport.id, field);
- const rawOptions = field.dynamicOptions
- ? (dynamicOptions[field.name] || field.options || [])
- : (field.options || []);
- const options = rawOptions.map((opt) => ({
- ...opt,
- label: optionLabel(currentReport.id, field.name, opt),
- }));
- const currentValue = criteria[field.name] || '';
- const valueForSelect = field.multiple
- ? (currentValue ? currentValue.split(',').map(v => v.trim()).filter(v => v) : [])
- : currentValue;
-
- // Use larger grid size for 成品/半成品生產分析報告
- const gridSize = currentReport.id === 'rep-005' ? { xs: 12, sm: 12, md: 6 } : { xs: 12, sm: 6 };
-
- const disabledByCheckedCheckbox = currentReport.fields.some((f) => {
- if (f.type !== 'checkbox' || criteria[f.name] !== 'true') return false;
- return f.disablesFieldsWhenChecked?.includes(field.name) ?? false;
- });
- const disabledRep012Status =
- currentReport.id === 'rep-012' &&
- field.name === 'status' &&
- rep012MultiRound;
-
- if (field.type === 'date') {
- const parsed = currentValue ? dayjs(currentValue) : null;
- const dateError = fieldErrors[field.name];
- return (
- <Grid item {...gridSize} key={fieldKey}>
- <DatePicker
- label={translatedLabel}
- format={dateDisplayFormat}
- value={parsed?.isValid() ? parsed : null}
- minDate={field.minDate === 'today' ? dayjs().startOf('day') : undefined}
- disabled={disabledByCheckedCheckbox || disabledRep012Status || !!field.disabled}
- onChange={(date) => {
- handleFieldChange(
- field.name,
- date?.isValid() ? date.format(OUTPUT_DATE_FORMAT) : '',
- );
- }}
- slotProps={{
- textField: {
- fullWidth: true,
- required: field.required,
- error: Boolean(dateError),
- helperText: dateError || undefined,
- sx: {
- ...FIELD_ERROR_SX,
- ...(currentReport.id === 'rep-005' ? {
- '& .MuiOutlinedInput-root': {
- minHeight: '64px',
- fontSize: '1rem'
- },
- '& .MuiInputLabel-root': {
- fontSize: '1rem'
- }
- } : {}),
- },
- },
- }}
- />
- </Grid>
- );
- }
-
- if (field.type === 'checkbox') {
- return (
- <Grid item {...gridSize} key={fieldKey}>
- <FormControlLabel
- control={
- <Checkbox
- checked={criteria[field.name] === 'true'}
- onChange={(e) =>
- handleFieldChange(field.name, e.target.checked ? 'true' : '')
- }
- />
- }
- label={translatedLabel}
- />
- </Grid>
- );
- }
-
- if (field.type === 'select' && field.allowInput && field.asyncSearch) {
- const selectedCodes = Array.isArray(valueForSelect) ? valueForSelect : [];
- return (
- <Grid item {...gridSize} key={fieldKey}>
- <AsyncItemCodeAutocomplete
- label={translatedLabel}
- placeholder={field.placeholder || "e.g. FA0591"}
- value={selectedCodes}
- onChange={(codes) => handleFieldChange(field.name, codes)}
- minChars={field.asyncSearchMinChars ?? 2}
- disabled={disabledByCheckedCheckbox || disabledRep012Status || !!field.disabled}
- />
- </Grid>
- );
- }
-
- // Use Autocomplete for fields that allow input
- if (field.type === 'select' && field.allowInput) {
- const autocompleteValue = field.multiple
- ? (Array.isArray(valueForSelect) ? valueForSelect : [])
- : (valueForSelect || null);
-
- return (
- <Grid item {...gridSize} key={fieldKey}>
- <Autocomplete
- multiple={field.multiple || false}
- freeSolo
- options={options.map(opt => opt.value)}
- value={autocompleteValue}
- onChange={(event, newValue, reason) => {
- if (field.multiple) {
- // Handle multiple selection - newValue is an array
- let values: string[] = [];
- if (Array.isArray(newValue)) {
- values = newValue
- .map(v => typeof v === 'string' ? v.trim() : String(v).trim())
- .filter(v => v !== '');
- }
- handleFieldChange(field.name, values);
- } else {
- // Handle single selection - newValue can be string or null
- const value = typeof newValue === 'string' ? newValue.trim() : (newValue || '');
- handleFieldChange(field.name, value);
- }
- }}
- onKeyDown={(event) => {
- // Allow Enter key to add custom value in multiple mode
- if (field.multiple && event.key === 'Enter') {
- const target = event.target as HTMLInputElement;
- if (target && target.value && target.value.trim()) {
- const currentValues = Array.isArray(autocompleteValue) ? autocompleteValue : [];
- const newValue = target.value.trim();
- if (!currentValues.includes(newValue)) {
- handleFieldChange(field.name, [...currentValues, newValue]);
- // Clear the input
- setTimeout(() => {
- if (target) target.value = '';
- }, 0);
- }
- }
- }
- }}
- renderInput={(params) => (
- <TextField
- {...params}
- fullWidth
- required={field.required}
- error={Boolean(fieldErrors[field.name])}
- helperText={fieldErrors[field.name] || undefined}
- label={translatedLabel}
- placeholder={field.placeholder || t('selectOrEnterItemCode')}
- sx={{
- ...FIELD_ERROR_SX,
- ...(currentReport.id === 'rep-005' ? {
- '& .MuiOutlinedInput-root': {
- minHeight: '64px',
- fontSize: '1rem'
- },
- '& .MuiInputLabel-root': {
- fontSize: '1rem'
- }
- } : {}),
- }}
- />
- )}
- renderTags={(value, getTagProps) =>
- value.map((option, index) => {
- // Find the label for the option if it exists in options
- const optionObj = options.find(opt => opt.value === option);
- const displayLabel = optionObj ? optionObj.label : String(option);
- return (
- <Chip
- variant="outlined"
- label={displayLabel}
- {...getTagProps({ index })}
- key={`${option}-${index}`}
- />
- );
- })
- }
- getOptionLabel={(option) => {
- // Find the label for the option if it exists in options
- const optionObj = options.find(opt => opt.value === option);
- return optionObj ? optionObj.label : String(option);
- }}
- />
- </Grid>
- );
- }
-
- // Regular TextField for other fields
- return (
- <Grid item {...gridSize} key={fieldKey}>
- <TextField
- fullWidth
- required={field.required}
- error={Boolean(fieldErrors[field.name])}
- helperText={fieldErrors[field.name] || undefined}
- label={translatedLabel}
- type={field.type}
- placeholder={field.placeholder}
- disabled={disabledByCheckedCheckbox || disabledRep012Status || !!field.disabled}
- sx={{
- ...FIELD_ERROR_SX,
- ...(currentReport.id === 'rep-005' ? {
- '& .MuiOutlinedInput-root': {
- minHeight: '64px',
- fontSize: '1rem'
- },
- '& .MuiInputLabel-root': {
- fontSize: '1rem'
- }
- } : {}),
- }}
- onChange={(e) => {
- if (field.multiple) {
- const value = typeof e.target.value === 'string'
- ? e.target.value.split(',')
- : e.target.value;
-
- // Special handling for stockCategory
- if (field.name === 'stockCategory' && Array.isArray(value)) {
- const currentValues = (criteria[field.name] || '').split(',').map(v => v.trim()).filter(v => v);
- const newValues = value.map(v => String(v).trim()).filter(v => v);
-
- const wasOnlyAll = currentValues.length === 1 && currentValues[0] === 'All';
- const hasAll = newValues.includes('All');
- const hasOthers = newValues.some(v => v !== 'All');
-
- if (hasAll && hasOthers) {
- // User selected "All" along with other options
- // If previously only "All" was selected, user is trying to switch - remove "All" and keep others
- if (wasOnlyAll) {
- const filteredValue = newValues.filter(v => v !== 'All');
- handleFieldChange(field.name, filteredValue);
- } else {
- // User added "All" to existing selections - keep only "All"
- handleFieldChange(field.name, ['All']);
- }
- } else if (hasAll && !hasOthers) {
- // Only "All" is selected
- handleFieldChange(field.name, ['All']);
- } else if (!hasAll && hasOthers) {
- // Other options selected without "All"
- handleFieldChange(field.name, newValues);
- } else {
- // Empty selection
- handleFieldChange(field.name, []);
- }
- } else {
- handleFieldChange(field.name, value);
- }
- } else {
- handleFieldChange(field.name, e.target.value);
- }
- }}
- value={valueForSelect}
- select={field.type === 'select'}
- SelectProps={field.multiple ? {
- multiple: true,
- renderValue: (selected: any) => {
- if (Array.isArray(selected)) {
- return selected
- .map((v) => {
- const opt = options.find((o) => o.value === v);
- return opt?.label ?? String(v);
- })
- .join(', ');
- }
- return selected;
- }
- } : {}}
- >
- {field.type === 'select' && options.map((opt) => (
- <MenuItem key={opt.value} value={opt.value}>
- {opt.label}
- </MenuItem>
- ))}
- </TextField>
- </Grid>
- );
- })}
- </Grid>
- </LocalizationProvider>
-
- <Box sx={{ mt: 4, display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
- {currentReport.id === 'rep-005' ? (
- <SemiFGProductionAnalysisReport
- criteria={criteria}
- requiredFieldLabels={currentReport.fields.filter(f => f.required && !criteria[f.name]).map(f => fieldLabel(currentReport.id, f))}
- loading={loading}
- setLoading={setLoading}
- reportTitle={reportTitle(currentReport)}
- onExportSuccess={(format) => {
- logFeatureUsage(
- FEATURE_USAGE.REPORT_MANAGEMENT,
- FEATURE_USAGE_ACTION.DOWNLOAD,
- `${currentReport.id}:${format}`,
- );
- }}
- />
- ) : currentReport.id === 'rep-013' || currentReport.id === 'rep-009' || currentReport.id === 'rep-012' || currentReport.id === 'rep-004' || currentReport.id === 'rep-007' || currentReport.id === 'rep-008' || currentReport.id === 'rep-011' ? (
- <>
- <Button
- variant="contained"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={handlePrint}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading ? t('generatingPdf') : t('downloadPdf')}
- </Button>
- <Button
- variant="outlined"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={handleExcelPrint}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading ? t('generatingExcel') : t('downloadExcel')}
- </Button>
- </>
- ) : currentReport.id === 'rep-006' || currentReport.id === 'rep-010' ? (
- <>
- <Button
- variant="contained"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={handlePrint}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading ? t('generatingPdf') : t('downloadPdf')}
- </Button>
- <Button
- variant="outlined"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={handleExcelPrint}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading ? t('generatingExcel') : t('downloadExcel')}
- </Button>
- </>
- ) : currentReport.responseType === 'excel' ? (
- <Button
- variant="contained"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={handlePrint}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading ? t('generatingExcel') : t('downloadExcel')}
- </Button>
- ) : (
- <Button
- variant="contained"
- size="large"
- startIcon={<DownloadIcon />}
- onClick={handlePrint}
- disabled={loading}
- sx={{ px: 4 }}
- >
- {loading ? t('generatingReport') : t('downloadPdf')}
- </Button>
- )}
- </Box>
- </CardContent>
- </Card>
- )}
- </Box>
- <Dialog
- open={showNoDataDialog}
- onClose={() => setShowNoDataDialog(false)}
- maxWidth="sm"
- fullWidth
- PaperProps={{
- sx: {
- borderRadius: 3,
- px: 1,
- },
- }}
- >
- <DialogTitle sx={{ display: 'flex', alignItems: 'center', gap: 1.5, pt: 3, px: 4, pb: 1.5 }}>
- <Box
- sx={{
- width: 36,
- height: 36,
- borderRadius: '50%',
- bgcolor: 'warning.light',
- color: 'warning.dark',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- flexShrink: 0,
- }}
- >
- <InfoOutlinedIcon fontSize="small" />
- </Box>
- <Typography component="span" variant="h6" fontWeight="bold">
- {t('noDataFoundTitle')}
- </Typography>
- </DialogTitle>
- <DialogContent sx={{ px: 4, pt: 0.5 }}>
- <Typography color="text.secondary" sx={{ pl: 6.5 }}>
- {t('noDataFoundHint')}
- </Typography>
- </DialogContent>
- <DialogActions sx={{ justifyContent: 'center', px: 4, pb: 3, pt: 2 }}>
- <Button variant="contained" onClick={() => setShowNoDataDialog(false)} autoFocus sx={{ minWidth: 96 }}>
- {t('ok')}
- </Button>
- </DialogActions>
- </Dialog>
- </>
- );
- }
|