diff --git a/.cursor/rules.md b/.cursor/rules.md new file mode 100644 index 0000000..23b2996 --- /dev/null +++ b/.cursor/rules.md @@ -0,0 +1,91 @@ +# Project Guidelines - Always Follow These Rules + +## UI Standard (apply to all pages) + +All pages under `(main)` must share the same look and feel. Use this as the single source of truth for new and existing pages. + +### Stack & layout + +- **Styling:** Tailwind CSS for layout and utilities. MUI components are used with the project theme (primary blue, neutral borders) so they match the standard. +- **Page wrapper:** Do **not** add a full-page wrapper with its own background or padding. The main layout (`src/app/(main)/layout.tsx`) already provides: + - Background: `bg-slate-50` (light), `dark:bg-slate-900` (dark) + - Padding: `p-4 sm:p-4 md:p-6 lg:p-8` +- **Responsive:** Mobile-first; use breakpoints `sm`, `md`, `lg` (e.g. `flex-col sm:flex-row`, `p-4 md:p-6 lg:p-8`). +- **Spacing:** Multiples of 4px only: `p-4`, `m-8`, `gap-2`, `gap-4`, `mb-4`, etc. + +### Theme & colors + +- **Default:** Light mode. Dark mode supported via `dark` class on `html`; use `dark:` Tailwind variants where needed. +- **Primary:** `#3b82f6` (blue) — main actions, links, focus rings. In MUI this is `palette.primary.main`. +- **Accent:** `#10b981` (emerald) — success, export, confirm actions. +- **Design tokens** are in `src/app/global.css` (`:root` / `.dark`): `--primary`, `--accent`, `--background`, `--foreground`, `--card`, `--border`, `--muted`. Use these in custom CSS or Tailwind when you need to stay in sync. + +### Page structure (every page) + +1. **Page title bar (consistent across all pages):** + - Use the shared **PageTitleBar** component from `@/components/PageTitleBar` so every menu destination has the same title style. + - It renders a bar with: left primary accent (4px), white/card background, padding, and title typography (`text-xl` / `sm:text-2xl`, bold, slate-900 / dark slate-100). + - **Usage:** `` or with actions: `...} className="mb-4" />`. + - Do **not** put a bare `

` or `` as the main page heading; use PageTitleBar for consistency. + +2. **Content:** Fragments or divs with `space-y-4` (or `Stack spacing={2}` in MUI) between sections. No extra full-width background wrapper. + +### Search criteria + +- **When using the shared SearchBox component:** It already uses the standard card style. Ensure the parent page does not wrap it in another card. +- **When building a custom search/query bar:** Use the shared class so it matches SearchBox: + - Wrapper: `className="app-search-criteria ..."` (plus layout classes like `flex flex-wrap items-center gap-2 p-4`). + - Label for “Search criteria” style: `className="app-search-criteria-label"` if you need a small uppercase label. +- **Search button:** Primary action = blue (MUI `variant="contained"` `color="primary"`, or Tailwind `bg-blue-500 text-white`). Reset = outline with neutral border (e.g. MUI `variant="outlined"` with slate border, or Tailwind `border border-slate-300`). + +### Forms & inputs + +- **Standard look (enforced by MUI theme):** White background, border `#e2e8f0` (neutral 200), focus ring primary blue. Use MUI `TextField` / `FormControl` / date pickers as-is; the theme in `src/theme/devias-material-kit` already matches this. +- **Tailwind-only forms (e.g. /ps):** Use the same tokens: `border border-slate-300`, `bg-white`, `focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20`, `text-slate-900`, `placeholder-slate-400`. + +### Buttons + +- **Primary action:** Blue filled — MUI `variant="contained"` `color="primary"` or Tailwind `bg-blue-500 text-white hover:bg-blue-600`. +- **Secondary / cancel:** Outline, neutral — MUI `variant="outlined"` with border `#e2e8f0` / `#334155` text, or Tailwind `border border-slate-300 text-slate-700 hover:bg-slate-100`. +- **Accent (e.g. export, success):** Green — MUI `color="success"` or Tailwind `bg-emerald-500` / `text-emerald-600` for outline. +- **Spacing:** Use `gap-2` or `gap-4` between buttons; keep padding multiples of 4 (e.g. `px-4 py-2`). + +### Tables & grids + +- **Container:** Wrap tables/grids in a card-style container so they match across pages: + - MUI: `` (theme already uses 8px radius, neutral border). + - Tailwind: `rounded-lg border border-slate-200 bg-white shadow-sm`. +- **Data grid (MUI X DataGrid):** Use `StyledDataGrid` from `@/components/StyledDataGrid`. It applies header bg neutral[50], header text neutral[700], cell padding and borders to match the standard. +- **Table (MUI Table):** Use `SearchResults` when you have a paginated list; it uses `Paper variant="outlined"` and theme table styles (header bg, borders). +- **Header row:** Background `bg-slate-50` / `neutral[50]`, text `text-slate-700` / `neutral[700]`, font-weight 600, padding `px-4 py-3` or theme default. +- **Body rows:** Border `border-slate-200` / theme divider, hover `hover:bg-slate-50` / `action.hover`. + +### Cards & surfaces + +- **Standard card:** 8px radius, 1px border (`var(--border)` or `neutral[200]`), white background (`var(--card)`), light shadow (`0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)`). MUI `Card` and `Paper` are themed to match. +- **Search-criteria card:** Use class `app-search-criteria` (left 4px primary border, same radius and shadow as above). + +### Menu bar & sidebar + +- **App bar (top):** White background, 1px bottom border (`palette.divider`), no heavy shadow (`elevation={0}`). Toolbar with consistent min-height and horizontal padding. Profile and title use `text.secondary` and font-weight 600. +- **Sidebar (navigation drawer):** Same as cards: white background, 1px right border, light shadow. Logo area with padding and bottom border; nav list with 4px/8px margins, 8px border-radius on items. **Selected item:** primary light background tint, primary text/icon, font-weight 600. **Hover:** neutral hover background. Use `ListItemButton` with `mx: 1`, `minWidth: 40` on icons. Child items slightly smaller font (0.875rem). +- **Profile dropdown:** Menu with 8px radius, 1px border (outlined Paper). Dense list, padding on header and items. Sign out as `MenuItem`. +- **Selection logic:** Nav item is selected when `pathname === item.path` or `pathname.startsWith(item.path + "/")`. Parent with children expands on click; leaf items navigate via Link. +- **Icons:** Use one icon per menu item that matches the action or section (e.g. Dashboard, LocalShipping for delivery, CalendarMonth for scheduling, Settings for settings). Prefer distinct MUI icons so items are easy to scan; avoid reusing the same icon for many items. + +### Reference implementations + +- **/ps** — Tailwind-only: query bar (`app-search-criteria`), buttons, table container, modals. Good reference for Tailwind patterns. +- **/do** — SearchBox + StyledDataGrid inside Paper; page title on layout. Good reference for MUI + layout. +- **/jo** — SearchBox + SearchResults (Paper-wrapped table); page title on layout. Same layout and search pattern as /do. + +When adding a **new page**, reuse the same structure: rely on the main layout for background/padding, use one optional standard `

`, then SearchBox (or `app-search-criteria` for custom bars), then Paper-wrapped grid/table or other content, with buttons and forms following the rules above. + +### Checklist for new pages + +- [ ] No extra full-page wrapper (background/padding come from main layout). +- [ ] Page title: use `` (optional `actions`). Add `className="mb-4"` for spacing below. +- [ ] Search/filter: use `SearchBox` or a div with `className="app-search-criteria"` for the bar. +- [ ] Tables/grids: wrap in `Paper variant="outlined"` (MUI) or `rounded-lg border border-slate-200 bg-white shadow-sm` (Tailwind); use `StyledDataGrid` or `SearchResults` where applicable. +- [ ] Buttons: primary = blue contained, secondary = outlined neutral, accent = green for success/export. +- [ ] Spacing: multiples of 4px (`p-4`, `gap-2`, `mb-4`); responsive with `sm`/`md`/`lg`. diff --git a/src/app/(main)/do/edit/page.tsx b/src/app/(main)/do/edit/page.tsx index a9d8d94..a3200a5 100644 --- a/src/app/(main)/do/edit/page.tsx +++ b/src/app/(main)/do/edit/page.tsx @@ -1,38 +1,36 @@ import { SearchParams } from "@/app/utils/fetchUtil"; -import DoDetail from "@/components/DoDetail/DodetailWrapper"; +import DoDetail from "@/components/DoDetail/DoDetailWrapper"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Typography } from "@mui/material"; import { isArray } from "lodash"; import { Metadata } from "next"; import { notFound } from "next/navigation"; import { Suspense } from "react"; export const metadata: Metadata = { - title: "Edit Delivery Order Detail" -} + title: "Edit Delivery Order Detail", +}; type Props = SearchParams; const DoEdit: React.FC = async ({ searchParams }) => { - const { t } = await getServerI18n("do"); - const id = searchParams["id"]; + const { t } = await getServerI18n("do"); + const id = searchParams["id"]; - if (!id || isArray(id) || !isFinite(parseInt(id))) { - notFound(); - } + if (!id || isArray(id) || !isFinite(parseInt(id))) { + notFound(); + } - return ( - <> - - {t("Edit Delivery Order Detail")} - - - }> - - - - - ); -} + return ( + <> + + + }> + + + + + ); +}; export default DoEdit; \ No newline at end of file diff --git a/src/app/(main)/do/page.tsx b/src/app/(main)/do/page.tsx index 8560496..e1ef75d 100644 --- a/src/app/(main)/do/page.tsx +++ b/src/app/(main)/do/page.tsx @@ -2,7 +2,7 @@ // import { getServerI18n } from "@/i18n" import DoSearch from "../../../components/DoSearch"; import { getServerI18n } from "../../../i18n"; -import { Stack, Typography } from "@mui/material"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider } from "@/i18n"; import { Metadata } from "next"; import { Suspense } from "react"; @@ -16,13 +16,7 @@ const DeliveryOrder: React.FC = async () => { return ( <> - - + }> diff --git a/src/app/(main)/jo/edit/page.tsx b/src/app/(main)/jo/edit/page.tsx index 6868224..4a6b8ed 100644 --- a/src/app/(main)/jo/edit/page.tsx +++ b/src/app/(main)/jo/edit/page.tsx @@ -1,52 +1,51 @@ import { fetchJoDetail } from "@/app/api/jo"; import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; import JoSave from "@/components/JoSave"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Typography } from "@mui/material"; import { isArray } from "lodash"; import { Metadata } from "next"; import { notFound } from "next/navigation"; import { Suspense } from "react"; export const metadata: Metadata = { - title: "Edit Job Order Detail" -} + title: "Edit Job Order Detail", +}; type Props = SearchParams; const JoEdit: React.FC = async ({ searchParams }) => { - const { t } = await getServerI18n("jo"); - const id = searchParams["id"]; - - if (!id || isArray(id) || !isFinite(parseInt(id))) { - notFound(); - } - - try { - await fetchJoDetail(parseInt(id)) - } catch (e) { - - if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { - console.log("Job Order not found:", e); - } else { - console.error("Error fetching Job Order detail:", e); - } - notFound(); + const { t } = await getServerI18n("jo"); + const id = searchParams["id"]; + + if (!id || isArray(id) || !isFinite(parseInt(id))) { + notFound(); + } + + try { + await fetchJoDetail(parseInt(id)); + } catch (e) { + if ( + e instanceof ServerFetchError && + (e.response?.status === 404 || e.response?.status === 400) + ) { + console.log("Job Order not found:", e); + } else { + console.error("Error fetching Job Order detail:", e); } - - - return ( - <> - - {t("Edit Job Order Detail")} - - - }> - - - - - ); -} + notFound(); + } + + return ( + <> + + + }> + + + + + ); +}; export default JoEdit; \ No newline at end of file diff --git a/src/app/(main)/jo/page.tsx b/src/app/(main)/jo/page.tsx index 6e2f73a..d794c57 100644 --- a/src/app/(main)/jo/page.tsx +++ b/src/app/(main)/jo/page.tsx @@ -1,38 +1,29 @@ import { preloadBomCombo } from "@/app/api/bom"; import JoSearch from "@/components/JoSearch"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Stack, Typography } from "@mui/material"; import { Metadata } from "next"; import React, { Suspense } from "react"; export const metadata: Metadata = { - title: "Job Order" -} + title: "Job Order", +}; -const jo: React.FC = async () => { - const { t } = await getServerI18n("jo"); +const Jo: React.FC = async () => { + const { t } = await getServerI18n("jo"); - preloadBomCombo() + preloadBomCombo(); - return ( - <> - - - {t("Search Job Order/ Create Job Order")} - - - {/* TODO: Improve */} - }> - - - - - ) -} + return ( + <> + + + }> + + + + + ); +}; -export default jo; \ No newline at end of file +export default Jo; \ No newline at end of file diff --git a/src/app/(main)/jodetail/edit/page.tsx b/src/app/(main)/jodetail/edit/page.tsx index 5172798..43a8027 100644 --- a/src/app/(main)/jodetail/edit/page.tsx +++ b/src/app/(main)/jodetail/edit/page.tsx @@ -1,8 +1,8 @@ import { fetchJoDetail } from "@/app/api/jo"; import { SearchParams, ServerFetchError } from "@/app/utils/fetchUtil"; import JoSave from "@/components/JoSave/JoSave"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Typography } from "@mui/material"; import { isArray } from "lodash"; import { Metadata } from "next"; import { notFound } from "next/navigation"; @@ -10,40 +10,41 @@ import { Suspense } from "react"; import GeneralLoading from "@/components/General/GeneralLoading"; export const metadata: Metadata = { - title: "Edit Job Order Detail" -} + title: "Edit Job Order Detail", +}; type Props = SearchParams; -const JoEdit: React.FC = async ({ searchParams }) => { - const { t } = await getServerI18n("jo"); - const id = searchParams["id"]; +const JodetailEdit: React.FC = async ({ searchParams }) => { + const { t } = await getServerI18n("jo"); + const id = searchParams["id"]; - if (!id || isArray(id) || !isFinite(parseInt(id))) { - notFound(); - } + if (!id || isArray(id) || !isFinite(parseInt(id))) { + notFound(); + } - try { - await fetchJoDetail(parseInt(id)) - } catch (e) { - if (e instanceof ServerFetchError && (e.response?.status === 404 || e.response?.status === 400)) { - console.log(e) - notFound(); - } + try { + await fetchJoDetail(parseInt(id)); + } catch (e) { + if ( + e instanceof ServerFetchError && + (e.response?.status === 404 || e.response?.status === 400) + ) { + console.log(e); + notFound(); } + } - return ( - <> - - {t("Edit Job Order Detail")} - - - }> - - - - - ); -} + return ( + <> + + + }> + + + + + ); +}; -export default JoEdit; \ No newline at end of file +export default JodetailEdit; \ No newline at end of file diff --git a/src/app/(main)/jodetail/page.tsx b/src/app/(main)/jodetail/page.tsx index 37a61eb..7769148 100644 --- a/src/app/(main)/jodetail/page.tsx +++ b/src/app/(main)/jodetail/page.tsx @@ -1,39 +1,30 @@ import { preloadBomCombo } from "@/app/api/bom"; -import JodetailSearch from "@/components/Jodetail/JodetailSearch"; +import JodetailSearchWrapper from "@/components/Jodetail/FinishedGoodSearchWrapper"; +import GeneralLoading from "@/components/General/GeneralLoading"; +import PageTitleBar from "@/components/PageTitleBar"; import { I18nProvider, getServerI18n } from "@/i18n"; -import { Stack, Typography } from "@mui/material"; import { Metadata } from "next"; import React, { Suspense } from "react"; -import GeneralLoading from "@/components/General/GeneralLoading"; -import JodetailSearchWrapper from "@/components/Jodetail/FinishedGoodSearchWrapper"; + export const metadata: Metadata = { - title: "Job Order Pickexcution" -} + title: "Job Order Pick Execution", +}; -const jo: React.FC = async () => { - const { t } = await getServerI18n("jo"); +const Jodetail: React.FC = async () => { + const { t } = await getServerI18n("jo"); - preloadBomCombo() + preloadBomCombo(); - return ( - <> - - - {t("Job Order Pickexcution")} - - - - }> - - - - - ) -} + return ( + <> + + + }> + + + + + ); +}; -export default jo; \ No newline at end of file +export default Jodetail; \ No newline at end of file diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index b8feb8f..45b6396 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -49,8 +49,8 @@ export default async function MainLayout({ component="main" sx={{ marginInlineStart: { xs: 0, xl: NAVIGATION_CONTENT_WIDTH }, - padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" }, }} + className="min-h-screen bg-slate-50 p-4 sm:p-4 md:p-6 lg:p-8 dark:bg-slate-900" > diff --git a/src/app/(main)/ps/page.tsx b/src/app/(main)/ps/page.tsx index 0d74e1c..675380c 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -1,92 +1,63 @@ "use client"; import React, { useState, useEffect, useMemo } from "react"; -import { - Box, Paper, Typography, Button, Dialog, DialogTitle, - DialogContent, DialogActions, TextField, Stack, Table, - TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, - CircularProgress, Tooltip, DialogContentText -} from "@mui/material"; -import { - Search, Visibility, ListAlt, CalendarMonth, - OnlinePrediction, FileDownload, SettingsEthernet -} from "@mui/icons-material"; +import Search from "@mui/icons-material/Search"; +import Visibility from "@mui/icons-material/Visibility"; +import FormatListNumbered from "@mui/icons-material/FormatListNumbered"; +import ShowChart from "@mui/icons-material/ShowChart"; +import Download from "@mui/icons-material/Download"; +import Hub from "@mui/icons-material/Hub"; +import { CircularProgress } from "@mui/material"; +import PageTitleBar from "@/components/PageTitleBar"; import dayjs from "dayjs"; -import { redirect } from "next/navigation"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; export default function ProductionSchedulePage() { - // ── Main states ── - const [searchDate, setSearchDate] = useState(dayjs().format('YYYY-MM-DD')); + const [searchDate, setSearchDate] = useState(dayjs().format("YYYY-MM-DD")); const [schedules, setSchedules] = useState([]); const [selectedLines, setSelectedLines] = useState([]); const [isDetailOpen, setIsDetailOpen] = useState(false); - const [selectedPs, setSelectedPs] = useState(null); + const [selectedPs, setSelectedPs] = useState(null); const [loading, setLoading] = useState(false); const [isGenerating, setIsGenerating] = useState(false); - // Forecast dialog const [isForecastDialogOpen, setIsForecastDialogOpen] = useState(false); - const [forecastStartDate, setForecastStartDate] = useState(dayjs().format('YYYY-MM-DD')); - const [forecastDays, setForecastDays] = useState(7); // default 7 days + const [forecastStartDate, setForecastStartDate] = useState( + dayjs().format("YYYY-MM-DD") + ); + const [forecastDays, setForecastDays] = useState(7); - // Export dialog const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); - const [exportFromDate, setExportFromDate] = useState(dayjs().format('YYYY-MM-DD')); + const [exportFromDate, setExportFromDate] = useState( + dayjs().format("YYYY-MM-DD") + ); - // Auto-search on mount useEffect(() => { handleSearch(); }, []); - // ── Formatters & Helpers ── const formatBackendDate = (dateVal: any) => { if (Array.isArray(dateVal)) { const [year, month, day] = dateVal; - return dayjs(new Date(year, month - 1, day)).format('DD MMM (dddd)'); + return dayjs(new Date(year, month - 1, day)).format("DD MMM (dddd)"); } - return dayjs(dateVal).format('DD MMM (dddd)'); + return dayjs(dateVal).format("DD MMM (dddd)"); }; const formatNum = (num: any) => { - return new Intl.NumberFormat('en-US').format(Number(num) || 0); + return new Intl.NumberFormat("en-US").format(Number(num) || 0); }; - const isDateToday = useMemo(() => { - if (!selectedPs?.produceAt) return false; - const todayStr = dayjs().format('YYYY-MM-DD'); - let scheduleDateStr = ""; - - if (Array.isArray(selectedPs.produceAt)) { - const [y, m, d] = selectedPs.produceAt; - scheduleDateStr = dayjs(new Date(y, m - 1, d)).format('YYYY-MM-DD'); - } else { - scheduleDateStr = dayjs(selectedPs.produceAt).format('YYYY-MM-DD'); - } - - return todayStr === scheduleDateStr; - }, [selectedPs]); - - // ── API Actions ── const handleSearch = async () => { - const token = localStorage.getItem("accessToken"); setLoading(true); - try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, { - method: 'GET', - headers: { 'Authorization': `Bearer ${token}` } - }); - - if (response.status === 401 || response.status === 403) { - console.warn(`Auth error ${response.status} → clearing token & redirecting`); - window.location.href = "/login?session=expired"; - - return; // ← stops execution here - } - + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, + { method: "GET" } + ); + if (response.status === 401 || response.status === 403) return; const data = await response.json(); - setSchedules(Array.isArray(data) ? data : []); } catch (e) { console.error("Search Error:", e); @@ -96,30 +67,22 @@ export default function ProductionSchedulePage() { }; const handleConfirmForecast = async () => { - if (!forecastStartDate || forecastDays === '' || forecastDays < 1) { + if (!forecastStartDate || forecastDays === "" || forecastDays < 1) { alert("Please enter a valid start date and number of days (≥1)."); return; } - - const token = localStorage.getItem("accessToken"); setLoading(true); setIsForecastDialogOpen(false); - try { const params = new URLSearchParams({ startDate: forecastStartDate, days: forecastDays.toString(), }); - const url = `${NEXT_PUBLIC_API_URL}/productionSchedule/testDetailedSchedule?${params.toString()}`; - - const response = await fetch(url, { - method: 'GET', - headers: { 'Authorization': `Bearer ${token}` } - }); - + const response = await clientAuthFetch(url, { method: "GET" }); + if (response.status === 401 || response.status === 403) return; if (response.ok) { - await handleSearch(); // refresh list + await handleSearch(); alert("成功計算排期!"); } else { const errorText = await response.text(); @@ -139,28 +102,21 @@ export default function ProductionSchedulePage() { alert("Please select a from date."); return; } - - const token = localStorage.getItem("accessToken"); setLoading(true); setIsExportDialogOpen(false); - try { - const params = new URLSearchParams({ - fromDate: exportFromDate, - }); - - const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, { - method: 'GET', // or keep POST if backend requires it - headers: { 'Authorization': `Bearer ${token}` } - }); - + const params = new URLSearchParams({ fromDate: exportFromDate }); + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, + { method: "GET" } + ); + if (response.status === 401 || response.status === 403) return; if (!response.ok) throw new Error(`Export failed: ${response.status}`); - const blob = await response.blob(); const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; - a.download = `production_schedule_from_${exportFromDate.replace(/-/g, '')}.xlsx`; + a.download = `production_schedule_from_${exportFromDate.replace(/-/g, "")}.xlsx`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); @@ -174,51 +130,24 @@ export default function ProductionSchedulePage() { }; const handleViewDetail = async (ps: any) => { - console.log("=== VIEW DETAIL CLICKED ==="); - console.log("Schedule ID:", ps?.id); - console.log("Full ps object:", ps); - if (!ps?.id) { alert("Cannot open details: missing schedule ID"); return; } - - const token = localStorage.getItem("accessToken"); - console.log("Token exists:", !!token); - setSelectedPs(ps); setLoading(true); - try { const url = `${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`; - console.log("Sending request to:", url); - - const response = await fetch(url, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - }, - }); - - console.log("Response status:", response.status); - console.log("Response ok?", response.ok); - + const response = await clientAuthFetch(url, { method: "GET" }); + if (response.status === 401 || response.status === 403) return; if (!response.ok) { const errorText = await response.text().catch(() => "(no text)"); - console.error("Server error response:", errorText); alert(`Server error ${response.status}: ${errorText}`); return; } - const data = await response.json(); - console.log("Full received lines (JSON):", JSON.stringify(data, null, 2)); - console.log("Received data type:", typeof data); - console.log("Received data:", data); - console.log("Number of lines:", Array.isArray(data) ? data.length : "not an array"); - setSelectedLines(Array.isArray(data) ? data : []); setIsDetailOpen(true); - } catch (err) { console.error("Fetch failed:", err); alert("Network or fetch error – check console"); @@ -227,27 +156,21 @@ export default function ProductionSchedulePage() { } }; - const handleAutoGenJob = async () => { - //if (!isDateToday) return; - const token = localStorage.getItem("accessToken"); setIsGenerating(true); try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ id: selectedPs.id }) - }); - + const response = await clientAuthFetch( + `${NEXT_PUBLIC_API_URL}/productionSchedule/detail/detailed/release`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: selectedPs.id }), + } + ); + if (response.status === 401 || response.status === 403) return; if (response.ok) { const data = await response.json(); - const displayMessage = data.message || "Operation completed."; - - alert(displayMessage); - //alert("Job Orders generated successfully!"); + alert(data.message || "Operation completed."); setIsDetailOpen(false); } else { alert("Failed to generate jobs."); @@ -260,263 +183,380 @@ export default function ProductionSchedulePage() { }; return ( - - - {/* Header */} - - - - 排程 - - - - - - - - - {/* Query Bar – unchanged */} - - + + + + + } + className="mb-4" + /> + + {/* Query Bar */} +
+ + setSearchDate(e.target.value)} + className="rounded-lg border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 placeholder-slate-400 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" /> - - - - {/* Main Table – unchanged */} - - - - - 詳細 - 生產日期 - 預計生產數 - 成品款數 - - - - {schedules.map((ps) => ( - - - handleViewDetail(ps)}> - - - - {formatBackendDate(ps.produceAt)} - {formatNum(ps.totalEstProdCount)} - {formatNum(ps.totalFGType)} - - ))} - -
-
- - {/* Detail Dialog – unchanged */} - setIsDetailOpen(false)} maxWidth="lg" fullWidth> - - - - 排期詳細: {selectedPs?.id} ({formatBackendDate(selectedPs?.produceAt)}) - - - - - - - - 工單號 - 物料編號 - 物料名稱 - 每日平均出貨量 - 出貨前預計存貨量 - 單位 - 可用日 - 生產量(批) - 預計生產包數 - 優先度 - - - - {selectedLines.map((line: any) => ( - - {line.joCode || '-'} - {line.itemCode} - {line.itemName} - {formatNum(line.avgQtyLastMonth)} - {formatNum(line.stockQty)} - {line.stockUnit} - + + + + {/* Main Table */} +
+
+
+ + + + + + + + + + {schedules.map((ps) => ( + + + + + + + ))} + +
+ 詳細 + + 生產日期 + + 預計生產數 + + 成品款數 +
+ + + {formatBackendDate(ps.produceAt)} + + {formatNum(ps.totalEstProdCount)} + + {formatNum(ps.totalFGType)} +
+
+ + + {/* Detail Modal – z-index above sidebar drawer (1200) so they don't overlap on small windows */} + {isDetailOpen && ( +
+
!isGenerating && setIsDetailOpen(false)} + /> +
+
+ +

+ 排期詳細: {selectedPs?.id} ( + {formatBackendDate(selectedPs?.produceAt)}) +

+
+
+ + + + + + + + + + + + + + + + + {selectedLines.map((line: any) => ( + + + + + + + +
+ 工單號 + + 物料編號 + + 物料名稱 + + 每日平均出貨量 + + 出貨前預計存貨量 + + 單位 + + 可用日 + + 生產量(批) + + 預計生產包數 + + 優先度 +
+ {line.joCode || "-"} + + {line.itemCode} + {line.itemName} + {formatNum(line.avgQtyLastMonth)} + + {formatNum(line.stockQty)} + {line.stockUnit} {line.daysLeft} - - {formatNum(line.batchNeed)} - {formatNum(line.prodQty)} - {line.itemPriority} - - ))} - -
- - - - {/* Footer Actions */} - - - {/* - - */} - -
+
+ + +
+
+
+ )} + + {/* Forecast Dialog */} + {isForecastDialogOpen && ( +
+
setIsForecastDialogOpen(false)} + /> +
+

+ 準備生成預計排期 +

+
+
+ + setForecastStartDate(e.target.value)} + min={dayjs().subtract(30, "day").format("YYYY-MM-DD")} + className="w-full rounded-lg border border-slate-300 bg-white px-3 py-2 text-slate-900 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-100" + /> +
+
+
+
+
+ + +
+
+
+ )} + + {/* Export Dialog */} + {isExportDialogOpen && ( +
+
setIsExportDialogOpen(false)} + /> +
+

+ 匯出排期/物料用量預計 +

+

+ 選擇要匯出的起始日期 +

+
+
+ )} +
); -} \ No newline at end of file +} diff --git a/src/app/(main)/report/page.tsx b/src/app/(main)/report/page.tsx index 100c194..f170845 100644 --- a/src/app/(main)/report/page.tsx +++ b/src/app/(main)/report/page.tsx @@ -17,6 +17,7 @@ import { import PrintIcon from '@mui/icons-material/Print'; import { REPORTS, ReportDefinition } from '@/config/reportConfig'; import { NEXT_PUBLIC_API_URL } from '@/config/api'; +import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; import SemiFGProductionAnalysisReport from './SemiFGProductionAnalysisReport'; import { fetchSemiFGItemCodes, @@ -90,25 +91,17 @@ export default function ReportPage() { } // Handle other reports with dynamic options - const token = localStorage.getItem("accessToken"); - - // Handle multiple stockCategory values (comma-separated) - // If "All" is included or no value, fetch all - // Otherwise, fetch for all selected categories let url = field.dynamicOptionsEndpoint; if (paramValue && paramValue !== 'All' && !paramValue.includes('All')) { - // Multiple categories selected (e.g., "FG,WIP") url = `${field.dynamicOptionsEndpoint}?${field.dynamicOptionsParam}=${paramValue}`; } - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + 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(); @@ -159,21 +152,18 @@ export default function ReportPage() { const executePrint = async () => { if (!currentReport) 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, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/pdf', - }, + 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); diff --git a/src/app/(main)/report/semiFGProductionAnalysisApi.ts b/src/app/(main)/report/semiFGProductionAnalysisApi.ts index 04cfc8a..ba7a1d7 100644 --- a/src/app/(main)/report/semiFGProductionAnalysisApi.ts +++ b/src/app/(main)/report/semiFGProductionAnalysisApi.ts @@ -1,4 +1,7 @@ +"use client"; + import { NEXT_PUBLIC_API_URL } from '@/config/api'; +import { clientAuthFetch } from '@/app/utils/clientAuthFetch'; export interface ItemCodeWithName { code: string; @@ -19,24 +22,18 @@ export interface ItemCodeWithCategory { export const fetchSemiFGItemCodes = async ( stockCategory: string = '' ): Promise => { - const token = localStorage.getItem("accessToken"); - let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes`; if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { url = `${url}?stockCategory=${stockCategory}`; } - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); }; @@ -49,24 +46,18 @@ export const fetchSemiFGItemCodes = async ( export const fetchSemiFGItemCodesWithCategory = async ( stockCategory: string = '' ): Promise => { - const token = localStorage.getItem("accessToken"); - let url = `${NEXT_PUBLIC_API_URL}/report/semi-fg-item-codes-with-category`; if (stockCategory && stockCategory !== 'All' && !stockCategory.includes('All')) { url = `${url}?stockCategory=${stockCategory}`; } - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, + headers: { 'Content-Type': 'application/json' }, }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); }; @@ -81,21 +72,16 @@ export const generateSemiFGProductionAnalysisReport = async ( criteria: Record, reportTitle: string = '成品/半成品生產分析報告' ): Promise => { - const token = localStorage.getItem("accessToken"); const queryParams = new URLSearchParams(criteria).toString(); const url = `${NEXT_PUBLIC_API_URL}/report/print-semi-fg-production-analysis?${queryParams}`; - - const response = await fetch(url, { + + const response = await clientAuthFetch(url, { method: 'GET', - headers: { - 'Authorization': `Bearer ${token}`, - 'Accept': 'application/pdf', - }, + headers: { 'Accept': 'application/pdf' }, }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } + if (response.status === 401 || response.status === 403) throw new Error("Unauthorized"); + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const blob = await response.blob(); const downloadUrl = window.URL.createObjectURL(blob); diff --git a/src/app/(main)/testing/page.tsx b/src/app/(main)/testing/page.tsx index 3efaf70..ae22386 100644 --- a/src/app/(main)/testing/page.tsx +++ b/src/app/(main)/testing/page.tsx @@ -10,6 +10,7 @@ import { import { FileDownload, Print, SettingsEthernet, Lan, Router } from "@mui/icons-material"; import dayjs from "dayjs"; import { NEXT_PUBLIC_API_URL } from "@/config/api"; +import { clientAuthFetch } from "@/app/utils/clientAuthFetch"; // Simple TabPanel component for conditional rendering interface TabPanelProps { @@ -97,14 +98,14 @@ export default function TestingPage() { // TSC Print (Section 1) const handleTscPrint = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: tscConfig.ip, printerPort: tscConfig.port }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-tsc`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-tsc`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; if (response.ok) alert(`TSC Print Command Sent for ${row.itemCode}!`); else alert("TSC Print Failed"); } catch (e) { console.error("TSC Error:", e); } @@ -112,14 +113,14 @@ export default function TestingPage() { // DataFlex Print (Section 2) const handleDfPrint = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: dfConfig.ip, printerPort: dfConfig.port }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-dataflex`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; if (response.ok) alert(`DataFlex Print Command Sent for ${row.itemCode}!`); else alert("DataFlex Print Failed"); } catch (e) { console.error("DataFlex Error:", e); } @@ -127,14 +128,13 @@ export default function TestingPage() { // OnPack Zip Download (Section 3) const handleDownloadPrintJob = async () => { - const token = localStorage.getItem("accessToken"); const params = new URLSearchParams(printerFormData); try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${params.toString()}`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/get-printer6?${params.toString()}`, { method: 'GET', - headers: { 'Authorization': `Bearer ${token}` } }); + if (response.status === 401 || response.status === 403) return; if (!response.ok) throw new Error('Download failed'); const blob = await response.blob(); @@ -153,34 +153,33 @@ export default function TestingPage() { // Laser Print (Section 4 - original) const handleLaserPrint = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: laserConfig.ip, printerPort: laserConfig.port }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; if (response.ok) alert(`Laser Command Sent: ${row.templateId}`); } catch (e) { console.error(e); } }; const handleLaserPreview = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { ...row, printerIp: laserConfig.ip, printerPort: parseInt(laserConfig.port) }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/preview-laser`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; if (response.ok) alert("Red light preview active!"); } catch (e) { console.error("Preview Error:", e); } }; // HANS600S-M TCP Print (Section 5) const handleHansPrint = async (row: any) => { - const token = localStorage.getItem("accessToken"); const payload = { printerIp: hansConfig.ip, printerPort: hansConfig.port, @@ -190,11 +189,12 @@ export default function TestingPage() { text4ObjectName: row.text4ObjectName }; try { - const response = await fetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, { + const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/plastic/print-laser-tcp`, { method: 'POST', - headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); + if (response.status === 401 || response.status === 403) return; const result = await response.text(); if (response.ok) { alert(`HANS600S-M Mark Success: ${result}`); diff --git a/src/app/api/jo/actions.ts b/src/app/api/jo/actions.ts index 5c33b1c..0aa7986 100644 --- a/src/app/api/jo/actions.ts +++ b/src/app/api/jo/actions.ts @@ -1201,9 +1201,15 @@ export interface MaterialPickStatusItem { pickStatus: string | null; } -export const fetchMaterialPickStatus = cache(async (): Promise => { +export const fetchMaterialPickStatus = cache(async (date?: string): Promise => { + const params = new URLSearchParams(); + if (date) params.set("date", date); // yyyy-MM-dd + + const qs = params.toString(); + const url = `${BASE_API_URL}/jo/material-pick-status${qs ? `?${qs}` : ""}`; + return await serverFetchJson( - `${BASE_API_URL}/jo/material-pick-status`, + url, { method: "GET", } diff --git a/src/app/api/qc/index.ts b/src/app/api/qc/index.ts index b862d63..f079ec5 100644 --- a/src/app/api/qc/index.ts +++ b/src/app/api/qc/index.ts @@ -29,9 +29,9 @@ export interface QcData { name?: string, order?: number, description?: string, - // qcPassed: boolean | undefined - // failQty: number | undefined - // remarks: string | undefined + qcPassed?: boolean, + failQty?: number, + remarks?: string, } export interface QcResult extends QcData{ id?: number; diff --git a/src/app/api/settings/m18ImportTesting/actions.ts b/src/app/api/settings/m18ImportTesting/actions.ts index 4a9bc80..7fbdffc 100644 --- a/src/app/api/settings/m18ImportTesting/actions.ts +++ b/src/app/api/settings/m18ImportTesting/actions.ts @@ -2,7 +2,7 @@ // import { serverFetchWithNoContent } from '@/app/utils/fetchUtil'; // import { BASE_API_URL } from "@/config/api"; -import { serverFetchWithNoContent } from "../../../utils/fetchUtil"; +import { serverFetch, serverFetchWithNoContent } from "../../../utils/fetchUtil"; import { BASE_API_URL } from "../../../../config/api"; export interface M18ImportPoForm { @@ -85,13 +85,13 @@ export const triggerScheduler = async (type: 'po' | 'do1' | 'do2' | 'master-data console.log("Fetching URL:", url); - const response = await serverFetchWithNoContent(url, { + const response = await serverFetch(url, { method: "GET", cache: "no-store", }); if (!response.ok) throw new Error(`Failed: ${response.status}`); - + return await response.text(); } catch (error) { console.error("Scheduler Action Error:", error); @@ -103,13 +103,13 @@ export const refreshCronSchedules = async () => { // Simply reuse the triggerScheduler logic to avoid duplication // or call serverFetch directly as shown below: try { - const response = await serverFetchWithNoContent(`${BASE_API_URL}/scheduler/refresh-cron`, { + const response = await serverFetch(`${BASE_API_URL}/scheduler/refresh-cron`, { method: "GET", cache: "no-store", }); if (!response.ok) throw new Error(`Failed to refresh: ${response.status}`); - + return await response.text(); } catch (error) { console.error("Refresh Cron Error:", error); diff --git a/src/app/api/stockIssue/actions.ts b/src/app/api/stockIssue/actions.ts index 790c4fe..5d52d32 100644 --- a/src/app/api/stockIssue/actions.ts +++ b/src/app/api/stockIssue/actions.ts @@ -25,6 +25,7 @@ export interface StockIssueResult { handleStatus: string; handleDate: string | null; handledBy: number | null; + uomDesc: string | null; } export interface ExpiryItemResult { id: number; @@ -178,6 +179,8 @@ export async function submitMissItem(issueId: number, handler: number) { itemDescription: string | null; storeLocation: string | null; issues: IssueDetailItem[]; + bookQty: number; + uomDesc: string | null; } export interface IssueDetailItem { diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index 1fb5495..66c2ef4 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -13,10 +13,11 @@ export interface UserInputs { username: string; name: string; staffNo?: string; + locked?: boolean; addAuthIds?: number[]; removeAuthIds?: number[]; password?: string; - confirmPassword?: string; + confirmPassword?: string; } export interface PasswordInputs { diff --git a/src/app/global.css b/src/app/global.css index 7d2ff9a..2b2a85d 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -1,7 +1,75 @@ - +@tailwind base; @tailwind components; @tailwind utilities; -html, body { +/* UI standard: light default, primary #3b82f6, accent #10b981 */ +@layer base { + :root { + --primary: #3b82f6; + --accent: #10b981; + --background: #f8fafc; + --foreground: #0f172a; + --card: #ffffff; + --card-foreground: #0f172a; + --border: #e2e8f0; + --muted: #64748b; + } + .dark { + --background: #0f172a; + --foreground: #f1f5f9; + --card: #1e293b; + --card-foreground: #f1f5f9; + --border: #334155; + --muted: #94a3b8; + } +} + +html, +body { overscroll-behavior: none; -} \ No newline at end of file +} + +/* Tablet/mobile: stable layout when virtual keyboard opens */ +html { + /* Prefer dynamic viewport height so layout can adapt to keyboard (if browser resizes) */ + height: 100%; +} +body { + min-height: 100%; + min-height: 100dvh; + background-color: var(--background); + color: var(--foreground); +} + +/* Full-height containers: use dvh so keyboard doesn’t squash the layout when overlay is used */ +@media (max-width: 1024px) { + .min-h-screen { + min-height: 100dvh; + } +} + +/* Avoid iOS zoom on input focus (keep inputs ≥16px where possible) */ +@media (max-width: 1024px) { + input, + select, + textarea { + font-size: max(16px, 1rem); + } +} + +.app-search-criteria { + border-radius: 8px; + border: 1px solid var(--border); + border-left-width: 4px; + border-left-color: var(--primary); + background-color: var(--card); + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); +} + +.app-search-criteria-label { + font-size: 0.75rem; + font-weight: 500; + color: #334155; + text-transform: uppercase; + letter-spacing: 0.05em; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index dde610b..027afb7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,4 +1,4 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; // import { detectLanguage } from "@/i18n"; // import ThemeRegistry from "@/theme/ThemeRegistry"; import { detectLanguage } from "../i18n"; @@ -9,6 +9,14 @@ export const metadata: Metadata = { description: "FPSMS - xxxx Management System", }; +/** Tablet/mobile: virtual keyboard overlays content instead of resizing viewport (avoids "half screen gone"). */ +export const viewport: Viewport = { + width: "device-width", + initialScale: 1, + viewportFit: "cover", + interactiveWidget: "overlays-content", +}; + export default async function RootLayout({ children, }: { diff --git a/src/app/utils/clientAuthFetch.ts b/src/app/utils/clientAuthFetch.ts new file mode 100644 index 0000000..1bc8462 --- /dev/null +++ b/src/app/utils/clientAuthFetch.ts @@ -0,0 +1,31 @@ +"use client"; + +const LOGIN_REDIRECT = "/login?session=expired"; + +/** + * Client-side fetch that adds Bearer token from localStorage and redirects + * to /login?session=expired on 401 or 403 (session timeout / unauthorized). + * Use this for all authenticated API requests so session expiry is handled consistently. + */ +export async function clientAuthFetch( + input: RequestInfo | URL, + init?: RequestInit +): Promise { + const token = + typeof window !== "undefined" ? localStorage.getItem("accessToken") : null; + const headers = new Headers(init?.headers); + if (token) { + headers.set("Authorization", `Bearer ${token}`); + } + + const response = await fetch(input, { ...init, headers }); + + if (response.status === 401 || response.status === 403) { + if (typeof window !== "undefined") { + console.warn(`Auth error ${response.status} → redirecting to login`); + window.location.href = LOGIN_REDIRECT; + } + } + + return response; +} diff --git a/src/components/AppBar/AppBar.tsx b/src/components/AppBar/AppBar.tsx index 1c1cec3..cb70811 100644 --- a/src/components/AppBar/AppBar.tsx +++ b/src/components/AppBar/AppBar.tsx @@ -5,7 +5,7 @@ import Profile from "./Profile"; import Box from "@mui/material/Box"; import NavigationToggle from "./NavigationToggle"; import { I18nProvider } from "@/i18n"; -import { Divider, Grid, Typography } from "@mui/material"; +import { Typography } from "@mui/material"; import Breadcrumb from "@/components/Breadcrumb"; import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; @@ -17,23 +17,29 @@ export interface AppBarProps { const AppBar: React.FC = ({ avatarImageSrc, profileName }) => { return ( - - + + - + - + - + {profileName} diff --git a/src/components/AppBar/NavigationToggle.tsx b/src/components/AppBar/NavigationToggle.tsx index 9f61753..5144411 100644 --- a/src/components/AppBar/NavigationToggle.tsx +++ b/src/components/AppBar/NavigationToggle.tsx @@ -31,13 +31,16 @@ const NavigationToggle: React.FC = () => { - + ); diff --git a/src/components/AppBar/Profile.tsx b/src/components/AppBar/Profile.tsx index dc37351..dc6ed1d 100644 --- a/src/components/AppBar/Profile.tsx +++ b/src/components/AppBar/Profile.tsx @@ -35,24 +35,25 @@ const Profile: React.FC = ({ avatarImageSrc, profileName }) => { - + {profileName} - signOut()}>{t("Sign out")} + signOut()} sx={{ py: 1.5, fontSize: "0.875rem" }}> + {t("Sign out")} + ); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 0ac5884..066d65c 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -14,6 +14,7 @@ const pathToLabelMap: { [path: string]: string } = { "/tasks": "Task Template", "/tasks/create": "Create Task Template", "/settings/qcItem": "Qc Item", + "/settings/qcItemAll": "QC Item All", "/settings/qrCodeHandle": "QR Code Handle", "/settings/rss": "Demand Forecast Setting", "/settings/equipment": "Equipment", diff --git a/src/components/CreateUser/CreateUser.tsx b/src/components/CreateUser/CreateUser.tsx index 4b6ea25..33b9e79 100644 --- a/src/components/CreateUser/CreateUser.tsx +++ b/src/components/CreateUser/CreateUser.tsx @@ -143,9 +143,10 @@ const CreateUser: React.FC = ({ rules, auths }) => { }); } } - const userData = { + const userData: UserInputs = { username: data.username, - // name: data.name, + name: data.name ?? "", + staffNo: data.staffNo, locked: false, addAuthIds: data.addAuthIds || [], removeAuthIds: data.removeAuthIds || [], diff --git a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx index ddccc5a..6c32223 100644 --- a/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx +++ b/src/components/DetailedSchedule/DetailedScheduleSearchView.tsx @@ -26,7 +26,7 @@ import isToday from 'dayjs/plugin/isToday'; import useUploadContext from "../UploadProvider/useUploadContext"; import { FileDownload, CalendarMonth } from "@mui/icons-material"; import { useSession } from "next-auth/react"; -import { VIEW_USER } from "@/authorities"; +import { AUTH } from "@/authorities"; dayjs.extend(isToday); @@ -384,7 +384,7 @@ const DSOverview: React.FC = ({ type, defaultInputs }) => { {t("Export Schedule")} - {false && abilities.includes(VIEW_USER) && ( + {false && abilities.includes(AUTH.VIEW_USER) && ( - )} + {hasSearched && hasResults && ( + + - - + )} { onReset={onReset} /> - { - setRowSelectionModel(newRowSelectionModel); - formProps.setValue("ids", newRowSelectionModel); - }} - slots={{ - footer: FooterToolbar, - noRowsOverlay: NoRowsOverlay, - }} - /> - - + + { + setRowSelectionModel(newRowSelectionModel); + formProps.setValue("ids", newRowSelectionModel); + }} + slots={{ + footer: FooterToolbar, + noRowsOverlay: NoRowsOverlay, + }} + /> + + diff --git a/src/components/FinishedGoodSearch/EscalationComponent.tsx b/src/components/FinishedGoodSearch/EscalationComponent.tsx index 53761a8..12e0b81 100644 --- a/src/components/FinishedGoodSearch/EscalationComponent.tsx +++ b/src/components/FinishedGoodSearch/EscalationComponent.tsx @@ -61,7 +61,7 @@ const EscalationComponent: React.FC = ({ ]; const handleInputChange = ( - event: ChangeEvent | SelectChangeEvent + event: ChangeEvent | SelectChangeEvent ): void => { const { name, value } = event.target; setFormData((prev) => ({ diff --git a/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx b/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx index 7cacf6e..9608cb6 100644 --- a/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx +++ b/src/components/FinishedGoodSearch/PickQcStockInModalVer2.tsx @@ -22,7 +22,7 @@ import { import { Dispatch, SetStateAction, useCallback, useEffect, useState } from "react"; import { FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { dummyQCData, QcData } from "../PoDetail/dummyQcTemplate"; +import { dummyQCData, QcData } from "./dummyQcTemplate"; import { submitDialogWithWarning } from "../Swal/CustomAlerts"; const style = { @@ -149,17 +149,17 @@ const PickQcStockInModalVer2: React.FC = ({ if (!qcData.qcItems.every((qc) => qc.isPassed) && qcData.qcAccept) { submitDialogWithWarning(() => { console.log("QC accepted with failed items"); - onClose(); + onClose?.({} as object, "backdropClick"); }, t, {title:"有不合格檢查項目,確認接受收貨?", confirmButtonText: "Confirm", html: ""}); return; } if (qcData.qcAccept) { console.log("QC accepted"); - onClose(); + onClose?.({} as object, "backdropClick"); } else { console.log("QC rejected"); - onClose(); + onClose?.({} as object, "backdropClick"); } }, [qcItems, onClose, t], @@ -260,7 +260,7 @@ const PickQcStockInModalVer2: React.FC = ({ color="warning" onClick={() => { console.log("Sort to accept"); - onClose(); + onClose?.({} as object, "backdropClick"); }} > Sort to Accept @@ -270,7 +270,7 @@ const PickQcStockInModalVer2: React.FC = ({ color="error" onClick={() => { console.log("Reject and pick another lot"); - onClose(); + onClose?.({} as object, "backdropClick"); }} > Reject and Pick Another Lot diff --git a/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx b/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx index 0c1a60a..8b9f723 100644 --- a/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx +++ b/src/components/FinishedGoodSearch/PickQcStockInModalVer3.tsx @@ -24,7 +24,7 @@ import { import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useState } from "react"; import { Controller, FormProvider, SubmitHandler, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; -import { dummyQCData } from "../PoDetail/dummyQcTemplate"; +import { dummyQCData } from "./dummyQcTemplate"; import StyledDataGrid from "../StyledDataGrid"; import { GridColDef } from "@mui/x-data-grid"; import { submitDialogWithWarning } from "../Swal/CustomAlerts"; diff --git a/src/components/FinishedGoodSearch/StockInFormVer2.tsx b/src/components/FinishedGoodSearch/StockInFormVer2.tsx index 32b9169..8c12c5c 100644 --- a/src/components/FinishedGoodSearch/StockInFormVer2.tsx +++ b/src/components/FinishedGoodSearch/StockInFormVer2.tsx @@ -111,7 +111,7 @@ const StockInFormVer2: React.FC = ({ if (isPickOrderData) { // PickOrder 数据 const pickOrderItem = itemDetail as GetPickOrderLineInfo & { pickOrderCode: string }; - return pickOrderItem.uomDesc || pickOrderItem.uomCode || ''; + return pickOrderItem.uomDesc || (pickOrderItem as { uomCode?: string }).uomCode || ''; } else { // StockIn 数据 const stockInItem = itemDetail as StockInLine; @@ -169,7 +169,7 @@ const StockInFormVer2: React.FC = ({ ReturnType)("itemNo", { required: "itemNo required!", })} value={getItemDisplayValue()} diff --git a/src/components/FinishedGoodSearch/newcreatitem copy.tsx b/src/components/FinishedGoodSearch/newcreatitem copy.tsx index dc94095..3a41014 100644 --- a/src/components/FinishedGoodSearch/newcreatitem copy.tsx +++ b/src/components/FinishedGoodSearch/newcreatitem copy.tsx @@ -1346,709 +1346,6 @@ const NewCreateItem: React.FC = ({ filterArgs, searchQuery, onPickOrderCr setSearchResultsPagingController(newPagingController); }, []); - // Add pagination state for created items - const [createdItemsPagingController, setCreatedItemsPagingController] = useState({ - pageNum: 1, - pageSize: 10, - }); - - // Add pagination handlers for created items - const handleCreatedItemsPageChange = useCallback((event: unknown, newPage: number) => { - const newPagingController = { - ...createdItemsPagingController, - pageNum: newPage + 1, - }; - setCreatedItemsPagingController(newPagingController); - }, [createdItemsPagingController]); - - const handleCreatedItemsPageSizeChange = useCallback((event: React.ChangeEvent) => { - const newPageSize = parseInt(event.target.value, 10); - const newPagingController = { - pageNum: 1, - pageSize: newPageSize, - }; - setCreatedItemsPagingController(newPagingController); - }, []); - - // Create a custom table for created items with pagination - const CustomCreatedItemsTable = () => { - const startIndex = (createdItemsPagingController.pageNum - 1) * createdItemsPagingController.pageSize; - const endIndex = startIndex + createdItemsPagingController.pageSize; - const paginatedCreatedItems = createdItems.slice(startIndex, endIndex); - - return ( - <> - - - - - - {t("Selected")} - - - {t("Item")} - - - {t("Group")} - - - {t("Current Stock")} - - - {t("Stock Unit")} - - - {t("Order Quantity")} - - - {t("Target Date")} - - - - - {paginatedCreatedItems.length === 0 ? ( - - - - {t("No created items")} - - - - ) : ( - paginatedCreatedItems.map((item) => ( - - - handleCreatedItemSelect(item.itemId, e.target.checked)} - /> - - - {item.itemName} - - {item.itemCode} - - - - - - - - - 0 ? "success.main" : "error.main"} - > - {item.currentStockBalance?.toLocaleString() || 0} - - - - {item.uomDesc} - - - { - const newQty = Number(e.target.value); - handleQtyChange(item.itemId, newQty); - }} - inputProps={{ - min: 1, - step: 1, - style: { textAlign: 'center' } - }} - sx={{ - width: '80px', - '& .MuiInputBase-input': { - textAlign: 'center', - cursor: 'text' - } - }} - /> - - - - {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} - - - - )) - )} - -
-
- - {/* Pagination for created items */} - - `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` - } - /> - - ); - }; - - // Define columns for SearchResults - const searchItemColumns: Column[] = useMemo(() => [ - { - name: "id", - label: "", - type: "checkbox", - disabled: (item) => isItemInCreated(item.id), // Disable if already in created items - }, - - { - name: "label", - label: t("Item"), - renderCell: (item) => { - - const parts = item.label.split(' - '); - const code = parts[0] || ''; - const name = parts[1] || ''; - - return ( - - - {name} {/* 显示项目名称 */} - - - {code} {/* 显示项目代码 */} - - - ); - }, - }, - { - name: "qty", - label: t("Order Quantity"), - renderCell: (item) => ( - { - const value = e.target.value; - const numValue = value === "" ? null : Number(value); - handleSearchQtyChange(item.id, numValue); - }} - inputProps={{ - min: 1, - step: 1, - style: { textAlign: 'center' } // Center the text - }} - sx={{ - width: '80px', - '& .MuiInputBase-input': { - textAlign: 'center', - cursor: 'text' - } - }} - /> - ), - }, - { - name: "currentStockBalance", - label: t("Current Stock"), - renderCell: (item) => { - const stockBalance = item.currentStockBalance || 0; - return ( - 0 ? "success.main" : "error.main"} - sx={{ fontWeight: stockBalance > 0 ? 'bold' : 'normal' }} - > - {stockBalance} - - ); - }, - }, - { - name: "targetDate", - label: t("Target Date"), - renderCell: (item) => ( - - {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} - - ), - }, - { - name: "uom", - label: t("Stock Unit"), - renderCell: (item) => item.uom || "-", - }, - ], [t, isItemInCreated, handleSearchQtyChange]); - // 修改搜索条件为3行,每行一个 - 确保SearchBox组件能正确处理 - const pickOrderSearchCriteria: Criterion[] = useMemo( - () => [ - - - { - label: t("Item Code"), - paramName: "code", - type: "text" - }, - { - label: t("Item Name"), - paramName: "name", - type: "text" - }, - { - label: t("Product Type"), - paramName: "type", - type: "autocomplete", - options: [ - { value: "Consumable", label: t("Consumable") }, - { value: "MATERIAL", label: t("Material") }, - { value: "End_product", label: t("End Product") } - ], - }, - ], - [t], - ); - - // 添加重置函数 - const handleSecondReset = useCallback(() => { - console.log("Second search reset"); - setSecondSearchQuery({}); - setSecondSearchResults([]); - setHasSearchedSecond(false); - // 清空表单中的类型,但保留今天的日期 - formProps.setValue("type", ""); - const today = dayjs().format(INPUT_DATE_FORMAT); - formProps.setValue("targetDate", today); - }, [formProps]); - - // 添加数量变更处理函数 - const handleSecondSearchQtyChange = useCallback((itemId: number, newQty: number | null) => { - setSecondSearchResults(prev => - prev.map(item => - item.id === itemId ? { ...item, qty: newQty } : item - ) - ); - - // Auto-update created items if this item exists there - setCreatedItems(prev => - prev.map(item => - item.itemId === itemId ? { ...item, qty: newQty || 1 } : item - ) - ); - }, []); - - // Add checkbox change handler for second search - const handleSecondSearchCheckboxChange = useCallback((ids: (string | number)[] | ((prev: (string | number)[]) => (string | number)[])) => { - if (typeof ids === 'function') { - const newIds = ids(selectedSecondSearchItemIds); - setSelectedSecondSearchItemIds(newIds); - - // 处理全选逻辑 - 选择所有搜索结果,不仅仅是当前页面 - if (newIds.length === secondSearchResults.length) { - // 全选:将所有搜索结果添加到创建项目 - secondSearchResults.forEach(item => { - if (!isItemInCreated(item.id)) { - handleSecondSearchItemSelect(item.id, true); - } - }); - } else { - // 部分选择:只处理当前页面的选择 - secondSearchResults.forEach(item => { - const isSelected = newIds.includes(item.id); - const isCurrentlyInCreated = isItemInCreated(item.id); - - if (isSelected && !isCurrentlyInCreated) { - handleSecondSearchItemSelect(item.id, true); - } else if (!isSelected && isCurrentlyInCreated) { - setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== item.id)); - } - }); - } - } else { - const previousIds = selectedSecondSearchItemIds; - setSelectedSecondSearchItemIds(ids); - - const newlySelected = ids.filter(id => !previousIds.includes(id)); - const newlyDeselected = previousIds.filter(id => !ids.includes(id)); - - newlySelected.forEach(id => { - if (!isItemInCreated(id as number)) { - handleSecondSearchItemSelect(id as number, true); - } - }); - - newlyDeselected.forEach(id => { - setCreatedItems(prev => prev.filter(createdItem => createdItem.itemId !== id)); - }); - } - }, [selectedSecondSearchItemIds, secondSearchResults, isItemInCreated, handleSecondSearchItemSelect]); - - // Update the secondSearchItemColumns to add right alignment for Current Stock and Order Quantity - const secondSearchItemColumns: Column[] = useMemo(() => [ - { - name: "id", - label: "", - type: "checkbox", - disabled: (item) => isItemInCreated(item.id), - }, - { - name: "label", - label: t("Item"), - renderCell: (item) => { - const parts = item.label.split(' - '); - const code = parts[0] || ''; - const name = parts[1] || ''; - - return ( - - - {name} - - - {code} - - - ); - }, - }, - { - name: "currentStockBalance", - label: t("Current Stock"), - align: "right", // Add right alignment for the label - renderCell: (item) => { - const stockBalance = item.currentStockBalance || 0; - return ( - - 0 ? "success.main" : "error.main"} - sx={{ - fontWeight: stockBalance > 0 ? 'bold' : 'normal', - textAlign: 'right' // Add right alignment for the value - }} - > - {stockBalance} - - - ); - }, - }, - { - name: "uom", - label: t("Stock Unit"), - align: "right", // Add right alignment for the label - renderCell: (item) => ( - - {/* Add right alignment for the value */} - {item.uom || "-"} - - - ), - }, - { - name: "qty", - label: t("Order Quantity"), - align: "right", - renderCell: (item) => ( - - { - const value = e.target.value; - // Only allow numbers - if (value === "" || /^\d+$/.test(value)) { - const numValue = value === "" ? null : Number(value); - handleSecondSearchQtyChange(item.id, numValue); - } - }} - inputProps={{ - style: { textAlign: 'center' } - }} - sx={{ - width: '80px', - '& .MuiInputBase-input': { - textAlign: 'center', - cursor: 'text' - } - }} - onBlur={(e) => { - const value = e.target.value; - const numValue = value === "" ? null : Number(value); - if (numValue !== null && numValue < 1) { - handleSecondSearchQtyChange(item.id, 1); // Enforce min value - } - }} - /> - - ), -} - ], [t, isItemInCreated, handleSecondSearchQtyChange, groups]); - - // 添加缺失的 handleSecondSearch 函数 - const handleSecondSearch = useCallback((query: Record) => { - console.log("Second search triggered with query:", query); - setSecondSearchQuery({ ...query }); - setIsLoadingSecondSearch(true); - - // Sync second search box info to form - ensure type value is correct - if (query.type) { - // Ensure type value matches backend enum format - let correctType = query.type; - if (query.type === "consumable") { - correctType = "Consumable"; - } else if (query.type === "material") { - correctType = "MATERIAL"; - } else if (query.type === "jo") { - correctType = "JOB_ORDER"; - } - formProps.setValue("type", correctType); - } - - setTimeout(() => { - let filtered = items; - - // Same filtering logic as first search - if (query.code && query.code.trim()) { - filtered = filtered.filter(item => - item.label.toLowerCase().includes(query.code.toLowerCase()) - ); - } - - if (query.name && query.name.trim()) { - filtered = filtered.filter(item => - item.label.toLowerCase().includes(query.name.toLowerCase()) - ); - } - - if (query.type && query.type !== "All") { - // Filter by type if needed - } - - // Convert to SearchItemWithQty with NO group/targetDate initially - const filteredWithQty = filtered.slice(0, 100).map(item => ({ - ...item, - qty: null, - targetDate: undefined, // No target date initially - groupId: undefined, // No group initially - })); - - setSecondSearchResults(filteredWithQty); - setHasSearchedSecond(true); - setIsLoadingSecondSearch(false); - }, 500); - }, [items, formProps]); - - // Add pagination state for search results - const [searchResultsPagingController, setSearchResultsPagingController] = useState({ - pageNum: 1, - pageSize: 10, - }); - - // Add pagination handlers for search results - const handleSearchResultsPageChange = useCallback((event: unknown, newPage: number) => { - const newPagingController = { - ...searchResultsPagingController, - pageNum: newPage + 1, // API uses 1-based pagination - }; - setSearchResultsPagingController(newPagingController); - }, [searchResultsPagingController]); - - const handleSearchResultsPageSizeChange = useCallback((event: React.ChangeEvent) => { - const newPageSize = parseInt(event.target.value, 10); - const newPagingController = { - pageNum: 1, // Reset to first page - pageSize: newPageSize, - }; - setSearchResultsPagingController(newPagingController); - }, []); - - // Add pagination state for created items - const [createdItemsPagingController, setCreatedItemsPagingController] = useState({ - pageNum: 1, - pageSize: 10, - }); - - // Add pagination handlers for created items - const handleCreatedItemsPageChange = useCallback((event: unknown, newPage: number) => { - const newPagingController = { - ...createdItemsPagingController, - pageNum: newPage + 1, - }; - setCreatedItemsPagingController(newPagingController); - }, [createdItemsPagingController]); - - const handleCreatedItemsPageSizeChange = useCallback((event: React.ChangeEvent) => { - const newPageSize = parseInt(event.target.value, 10); - const newPagingController = { - pageNum: 1, - pageSize: newPageSize, - }; - setCreatedItemsPagingController(newPagingController); - }, []); - - // Create a custom table for created items with pagination - const CustomCreatedItemsTable = () => { - const startIndex = (createdItemsPagingController.pageNum - 1) * createdItemsPagingController.pageSize; - const endIndex = startIndex + createdItemsPagingController.pageSize; - const paginatedCreatedItems = createdItems.slice(startIndex, endIndex); - - return ( - <> - - - - - - {t("Selected")} - - - {t("Item")} - - - {t("Group")} - - - {t("Current Stock")} - - - {t("Stock Unit")} - - - {t("Order Quantity")} - - - {t("Target Date")} - - - - - {paginatedCreatedItems.length === 0 ? ( - - - - {t("No created items")} - - - - ) : ( - paginatedCreatedItems.map((item) => ( - - - handleCreatedItemSelect(item.itemId, e.target.checked)} - /> - - - {item.itemName} - - {item.itemCode} - - - - - - - - - 0 ? "success.main" : "error.main"} - > - {item.currentStockBalance?.toLocaleString() || 0} - - - - {item.uomDesc} - - - { - const newQty = Number(e.target.value); - handleQtyChange(item.itemId, newQty); - }} - inputProps={{ - min: 1, - step: 1, - style: { textAlign: 'center' } - }} - sx={{ - width: '80px', - '& .MuiInputBase-input': { - textAlign: 'center', - cursor: 'text' - } - }} - /> - - - - {item.targetDate ? new Date(item.targetDate).toLocaleDateString() : "-"} - - - - )) - )} - -
-
- - {/* Pagination for created items */} - - `${from}-${to} of ${count !== -1 ? count : `more than ${to}`}` - } - /> - - ); - }; - // Add helper function to get group range text const getGroupRangeText = useCallback(() => { if (groups.length === 0) return ""; diff --git a/src/components/FinishedGoodSearch/pickorderModelVer2.tsx b/src/components/FinishedGoodSearch/pickorderModelVer2.tsx index 1294f1d..2bd870f 100644 --- a/src/components/FinishedGoodSearch/pickorderModelVer2.tsx +++ b/src/components/FinishedGoodSearch/pickorderModelVer2.tsx @@ -3,6 +3,7 @@ import { GetPickOrderLineInfo } from "@/app/api/pickOrder/actions"; import { QcItemWithChecks } from "@/app/api/qc"; import { PurchaseQcResult } from "@/app/api/po/actions"; +import { StockInLine } from "@/app/api/po"; import { Box, Button, @@ -187,7 +188,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) if (qcData.qcAccept) { onOpenPutaway(); } else { - onClose(); + onClose?.({} as object, "backdropClick"); } }, [onOpenPutaway, qcItems], @@ -281,7 +282,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) onSubmit={formProps.handleSubmit(onSubmitPutaway)} > @@ -341,7 +342,7 @@ const [qcItems, setQcItems] = useState(dummyQCData) > { display="flex" justifyContent="center" alignItems="center" - // autoheight="true" + >
diff --git a/src/components/InputDataGrid/InputDataGrid.tsx b/src/components/InputDataGrid/InputDataGrid.tsx index 51ebecf..bab891d 100644 --- a/src/components/InputDataGrid/InputDataGrid.tsx +++ b/src/components/InputDataGrid/InputDataGrid.tsx @@ -370,7 +370,7 @@ function InputDataGrid({ // columns={!checkboxSelection ? _columns : columns} columns={needActions ? _columns : columns} editMode="row" - // autoHeight + sx={{ height: "30vh", "--DataGrid-overlayHeight": "100px", diff --git a/src/components/ItemsSearch/ItemsSearch.tsx b/src/components/ItemsSearch/ItemsSearch.tsx index 112a95d..a230ae0 100644 --- a/src/components/ItemsSearch/ItemsSearch.tsx +++ b/src/components/ItemsSearch/ItemsSearch.tsx @@ -195,7 +195,7 @@ const ItemsSearch: React.FC = ({ items }) => { setFilterObj({ ...query, }); - refetchData(query); + refetchData(query as unknown as SearchQuery); }} onReset={onReset} /> diff --git a/src/components/JoSearch/JoSearch.tsx b/src/components/JoSearch/JoSearch.tsx index c9931c7..6655a92 100644 --- a/src/components/JoSearch/JoSearch.tsx +++ b/src/components/JoSearch/JoSearch.tsx @@ -23,7 +23,7 @@ import { SessionWithTokens } from "@/config/authConfig"; import { createStockInLine } from "@/app/api/stockIn/actions"; import { msg } from "../Swal/CustomAlerts"; import dayjs from "dayjs"; -import { fetchInventories } from "@/app/api/inventory/actions"; +//import { fetchInventories } from "@/app/api/inventory/actions"; import { InventoryResult } from "@/app/api/inventory"; import { PrinterCombo } from "@/app/api/settings/printer"; import { JobTypeResponse } from "@/app/api/jo/actions"; @@ -76,16 +76,21 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT useEffect(() => { const fetchDetailedJos = async () => { const detailedMap = new Map(); - - for (const jo of filteredJos) { - try { - const detailedJo = await fetchJoDetailClient(jo.id); - detailedMap.set(jo.id, detailedJo); - } catch (error) { - console.error(`Error fetching detail for JO ${jo.id}:`, error); - } + try { + const results = await Promise.all( + filteredJos.map((jo) => + fetchJoDetailClient(jo.id).then((detail) => ({ id: jo.id, detail })).catch((error) => { + console.error(`Error fetching detail for JO ${jo.id}:`, error); + return null; + }) + ) + ); + results.forEach((r) => { + if (r) detailedMap.set(r.id, r.detail); + }); + } catch (error) { + console.error("Error fetching JO details:", error); } - setDetailedJos(detailedMap); }; @@ -93,7 +98,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT fetchDetailedJos(); } }, [filteredJos]); - +/* useEffect(() => { const fetchInventoryData = async () => { try { @@ -102,9 +107,9 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT name: "", type: "", pageNum: 0, - pageSize: 1000 + pageSize: 200, }); - setInventoryData(inventoryResponse.records); + setInventoryData(inventoryResponse.records ?? []); } catch (error) { console.error("Error fetching inventory data:", error); } @@ -112,6 +117,7 @@ const JoSearch: React.FC = ({ defaultInputs, bomCombo, printerCombo, jobT fetchInventoryData(); }, []); + */ const getStockAvailable = (pickLine: JoDetailPickLine) => { const inventory = inventoryData.find(inventory => diff --git a/src/components/Jodetail/EscalationComponent.tsx b/src/components/Jodetail/EscalationComponent.tsx index 53761a8..12e0b81 100644 --- a/src/components/Jodetail/EscalationComponent.tsx +++ b/src/components/Jodetail/EscalationComponent.tsx @@ -61,7 +61,7 @@ const EscalationComponent: React.FC = ({ ]; const handleInputChange = ( - event: ChangeEvent | SelectChangeEvent + event: ChangeEvent | SelectChangeEvent ): void => { const { name, value } = event.target; setFormData((prev) => ({ diff --git a/src/components/Jodetail/FInishedJobOrderRecord.tsx b/src/components/Jodetail/FInishedJobOrderRecord.tsx index e076661..5f5a0a8 100644 --- a/src/components/Jodetail/FInishedJobOrderRecord.tsx +++ b/src/components/Jodetail/FInishedJobOrderRecord.tsx @@ -509,9 +509,11 @@ const FInishedJobOrderRecord: React.FC = ({ filterArgs }) => { size="small" sx={{ mb: 1 }} /> + {/* {jobOrderPickOrder.completedItems}/{jobOrderPickOrder.totalItems} {t("items completed")} + */} { type: undefined, status: undefined, itemName: undefined, + pageNum: 0, + pageSize: 50, }), fetchPrinterCombo(), ]); diff --git a/src/components/Jodetail/JobPickExecutionForm.tsx b/src/components/Jodetail/JobPickExecutionForm.tsx index 6d6a054..80b18e2 100644 --- a/src/components/Jodetail/JobPickExecutionForm.tsx +++ b/src/components/Jodetail/JobPickExecutionForm.tsx @@ -91,7 +91,9 @@ const PickExecutionForm: React.FC = ({ const [handlers, setHandlers] = useState>([]); const [verifiedQty, setVerifiedQty] = useState(0); const { data: session } = useSession() as { data: SessionWithTokens | null }; - + const missSet = formData.missQty != null; +const badItemSet = formData.badItemQty != null; +const badPackageSet = (formData as any).badPackageQty != null; const calculateRemainingAvailableQty = useCallback((lot: LotPickData) => { return lot.availableQty || 0; }, []); @@ -162,9 +164,9 @@ useEffect(() => { storeLocation: selectedLot.location, requiredQty: selectedLot.requiredQty, actualPickQty: initialVerifiedQty, - missQty: 0, - badItemQty: 0, - badPackageQty: 0, // Bad Package Qty (frontend only) + missQty: undefined, + badItemQty: undefined, + badPackageQty: undefined, issueRemark: "", pickerName: "", handledBy: undefined, @@ -195,10 +197,10 @@ useEffect(() => { const newErrors: FormErrors = {}; const ap = Number(verifiedQty) || 0; const miss = Number(formData.missQty) || 0; - const badItem = Number(formData.badItemQty) || 0; - const badPackage = Number((formData as any).badPackageQty) || 0; - const totalBad = badItem + badPackage; - const total = ap + miss + totalBad; + const badItem = Number(formData.badItemQty) ?? 0; + const badPackage = Number((formData as any).badPackageQty) ?? 0; + const totalBadQty = badItem + badPackage; + const total = ap + miss + totalBadQty; const availableQty = selectedLot?.availableQty || 0; // 1. Check actualPickQty cannot be negative @@ -231,7 +233,7 @@ useEffect(() => { } // 5. At least one field must have a value - if (ap === 0 && miss === 0 && totalBad === 0) { + if (ap === 0 && miss === 0 && totalBadQty === 0) { newErrors.actualPickQty = t("Enter pick qty or issue qty"); } @@ -245,10 +247,9 @@ useEffect(() => { // 增加 badPackageQty 判断,确保有坏包装会走 issue 流程 const badPackageQty = Number((formData as any).badPackageQty) || 0; - const isNormalPick = verifiedQty > 0 - && formData.missQty == 0 - && formData.badItemQty == 0 - && badPackageQty == 0; + const isNormalPick = (formData.missQty == null || formData.missQty === 0) + && (formData.badItemQty == null || formData.badItemQty === 0) + && (badPackageQty === 0); if (isNormalPick) { if (onNormalPickSubmit) { @@ -288,11 +289,12 @@ useEffect(() => { const submissionData: PickExecutionIssueData = { ...(formData as PickExecutionIssueData), actualPickQty: verifiedQty, - lotId: formData.lotId || selectedLot?.lotId || 0, - lotNo: formData.lotNo || selectedLot?.lotNo || '', - pickOrderCode: formData.pickOrderCode || selectedPickOrderLine?.pickOrderCode || '', - pickerName: session?.user?.name || '', - badItemQty: totalBadQty, + lotId: formData.lotId ?? selectedLot?.lotId ?? 0, + lotNo: formData.lotNo ?? selectedLot?.lotNo ?? '', + pickOrderCode: formData.pickOrderCode ?? selectedPickOrderLine?.pickOrderCode ?? '', + pickerName: session?.user?.name ?? '', + missQty: formData.missQty ?? 0, // 这里:null/undefined → 0 + badItemQty: totalBadQty, // totalBadQty 下面用 ?? 0 算 badReason, }; @@ -397,7 +399,8 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={formData.missQty || 0} + disabled={badItemSet || badPackageSet} + value={formData.missQty || ""} onChange={(e) => { handleInputChange( "missQty", @@ -421,7 +424,7 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={formData.badItemQty || 0} + value={formData.badItemQty || ""} onChange={(e) => { const newBadItemQty = e.target.value === "" ? undefined @@ -429,6 +432,7 @@ useEffect(() => { handleInputChange('badItemQty', newBadItemQty); }} error={!!errors.badItemQty} + disabled={missSet || badPackageSet} helperText={errors.badItemQty} variant="outlined" /> @@ -444,7 +448,7 @@ useEffect(() => { pattern: "[0-9]*", min: 0, }} - value={(formData as any).badPackageQty || 0} + value={(formData as any).badPackageQty || ""} onChange={(e) => { handleInputChange( "badPackageQty", @@ -453,6 +457,7 @@ useEffect(() => { : Math.max(0, Number(e.target.value) || 0) ); }} + disabled={missSet || badItemSet} error={!!errors.badItemQty} variant="outlined" /> diff --git a/src/components/Jodetail/JobPickExecutionsecondscan.tsx b/src/components/Jodetail/JobPickExecutionsecondscan.tsx index 823f79c..21163e1 100644 --- a/src/components/Jodetail/JobPickExecutionsecondscan.tsx +++ b/src/components/Jodetail/JobPickExecutionsecondscan.tsx @@ -868,7 +868,8 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { qty: submitQty, isMissing: false, isBad: false, - reason: undefined + reason: undefined, + userId: currentUserId ?? 0 } ); @@ -881,7 +882,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { } catch (error) { console.error("Error submitting second scan quantity:", error); } - }, [fetchJobOrderData]); + }, [fetchJobOrderData, currentUserId]); const handlePickExecutionForm = useCallback((lot: any) => { console.log("=== Pick Execution Form ==="); @@ -1263,55 +1264,24 @@ const JobPickExecution: React.FC = ({ filterArgs, onBack }) => { return requiredQty.toLocaleString()+'('+lot.uomShortDesc+')'; })()} - {/* - - {lot.matchStatus?.toLowerCase() === 'scanned' || - lot.matchStatus?.toLowerCase() === 'completed' ? ( - - - - ) : ( - - {t(" ")} - - )} - - */} + - - ))} @@ -675,6 +672,7 @@ const CompleteJobOrderRecord: React.FC = ({ onPageChange={handlePageChange} onRowsPerPageChange={handlePageSizeChange} rowsPerPageOptions={[5, 10, 25, 50]} + labelRowsPerPage={t("Rows per page")} /> )} diff --git a/src/components/Jodetail/newJobPickExecution.tsx b/src/components/Jodetail/newJobPickExecution.tsx index 4549061..3b9a9d8 100644 --- a/src/components/Jodetail/newJobPickExecution.tsx +++ b/src/components/Jodetail/newJobPickExecution.tsx @@ -1822,7 +1822,7 @@ const JobPickExecution: React.FC = ({ filterArgs, onBackToList }) => { }, [handleSubmitPickQtyWithQty]); const handleSubmitAllScanned = useCallback(async () => { const scannedLots = combinedLotData.filter(lot => - lot.stockOutLineStatus === 'checked' + lot.stockOutLineStatus === 'checked' || lot.stockOutLineStatus === 'partially_completed' ); if (scannedLots.length === 0) { diff --git a/src/components/LoginPage/LoginPage.tsx b/src/components/LoginPage/LoginPage.tsx index 73f4c8a..7be50c8 100644 --- a/src/components/LoginPage/LoginPage.tsx +++ b/src/components/LoginPage/LoginPage.tsx @@ -8,19 +8,33 @@ import { Box } from "@mui/material"; const LoginPage = () => { return ( - - - + + - + { display: "flex", alignItems: "flex-end", justifyContent: "center", - svg: { maxHeight: 120 }, + backgroundImage: "linear-gradient(135deg, rgba(59,130,246,0.15) 0%, #f1f5f9 45%, #f8fafc 100%)", + backgroundColor: "#f8fafc", }} > - + diff --git a/src/components/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index de2ea41..f8bc458 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -1,32 +1,126 @@ +"use client"; + interface Props { width?: number; height?: number; + className?: string; } -const Logo: React.FC = ({ width, height }) => { +/** Same logo height everywhere so login and main page look identical. */ +const DEFAULT_LOGO_HEIGHT = 42; + +/** + * Logo: rounded badge (FP) with links motif inside + FP-MTMS wordmark. + * Uses fixed typography so words look the same on login and main page. + */ +const Logo: React.FC = ({ height = DEFAULT_LOGO_HEIGHT, className = "" }) => { + const size = Math.max(28, height); + const badgeSize = Math.round(size * 0.7); + const titleFontSize = 21; + const subtitleFontSize = 10; + const fpSize = badgeSize <= 22 ? 10 : badgeSize <= 28 ? 12 : 14; + return ( - - + + + + + + + + + + + + + + + {/* Shadow */} + + {/* Body */} + + + {/* Links motif inside: small chain links in corners, clear center for FP */} + + + + + + + + + + + {/* FP text – top-right so it doesn’t overlap the links */} + + FP + + + {/* Wordmark: fixed typography so login and main page match */} +
- - - + + FP-MTMS + + + Food Production + +
+
); }; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 3709623..996a12c 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -1,31 +1,44 @@ import { useSession } from "next-auth/react"; -import Divider from "@mui/material/Divider"; import Box from "@mui/material/Box"; -import React, { useEffect } from "react"; +import React from "react"; import List from "@mui/material/List"; import ListItemButton from "@mui/material/ListItemButton"; import ListItemText from "@mui/material/ListItemText"; import ListItemIcon from "@mui/material/ListItemIcon"; -import WorkHistory from "@mui/icons-material/WorkHistory"; import Dashboard from "@mui/icons-material/Dashboard"; -import SummarizeIcon from "@mui/icons-material/Summarize"; -import PaymentsIcon from "@mui/icons-material/Payments"; -import AccountTreeIcon from "@mui/icons-material/AccountTree"; -import RequestQuote from "@mui/icons-material/RequestQuote"; -import PeopleIcon from "@mui/icons-material/People"; -import Task from "@mui/icons-material/Task"; +import Storefront from "@mui/icons-material/Storefront"; +import LocalShipping from "@mui/icons-material/LocalShipping"; import Assignment from "@mui/icons-material/Assignment"; -import Settings from "@mui/icons-material/Settings"; -import Analytics from "@mui/icons-material/Analytics"; -import Payments from "@mui/icons-material/Payments"; +import Inventory from "@mui/icons-material/Inventory"; +import AssignmentTurnedIn from "@mui/icons-material/AssignmentTurnedIn"; +import ReportProblem from "@mui/icons-material/ReportProblem"; import QrCodeIcon from "@mui/icons-material/QrCode"; +import ViewModule from "@mui/icons-material/ViewModule"; +import Description from "@mui/icons-material/Description"; +import CalendarMonth from "@mui/icons-material/CalendarMonth"; +import Factory from "@mui/icons-material/Factory"; +import PostAdd from "@mui/icons-material/PostAdd"; +import Kitchen from "@mui/icons-material/Kitchen"; +import Inventory2 from "@mui/icons-material/Inventory2"; +import Print from "@mui/icons-material/Print"; +import Assessment from "@mui/icons-material/Assessment"; +import Settings from "@mui/icons-material/Settings"; +import Person from "@mui/icons-material/Person"; +import Group from "@mui/icons-material/Group"; +import Category from "@mui/icons-material/Category"; +import TrendingUp from "@mui/icons-material/TrendingUp"; +import Build from "@mui/icons-material/Build"; +import Warehouse from "@mui/icons-material/Warehouse"; +import VerifiedUser from "@mui/icons-material/VerifiedUser"; +import Label from "@mui/icons-material/Label"; +import Checklist from "@mui/icons-material/Checklist"; +import Science from "@mui/icons-material/Science"; +import UploadFile from "@mui/icons-material/UploadFile"; import { useTranslation } from "react-i18next"; -import Typography from "@mui/material/Typography"; import { usePathname } from "next/navigation"; import Link from "next/link"; import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; -import BugReportIcon from "@mui/icons-material/BugReport"; import { AUTH } from "../../authorities"; interface NavigationItem { @@ -57,81 +70,55 @@ const NavigationContent: React.FC = () => { path: "/dashboard", }, { - icon: , + icon: , label: "Store Management", path: "", requiredAbility: [AUTH.PURCHASE, AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_FG, AUTH.STOCK_IN_BIND, AUTH.ADMIN], children: [ { - icon: , + icon: , label: "Purchase Order", requiredAbility: [AUTH.PURCHASE, AUTH.ADMIN], path: "/po", }, { - icon: , + icon: , label: "Pick Order", requiredAbility: [AUTH.STOCK, AUTH.ADMIN], path: "/pickOrder", }, - // { - // icon: , - // label: "Cons. Pick Order", - // path: "", - // }, - // { - // icon: , - // label: "Delivery Pick Order", - // path: "", - // }, - // { - // icon: , - // label: "Warehouse", - // path: "", - // }, - // { - // icon: , - // label: "Location Transfer Order", - // path: "", - // }, { - icon: , + icon: , label: "View item In-out And inventory Ledger", requiredAbility: [AUTH.STOCK, AUTH.ADMIN], path: "/inventory", }, { - icon: , + icon: , label: "Stock Take Management", requiredAbility: [AUTH.STOCK_TAKE, AUTH.ADMIN], path: "/stocktakemanagement", }, { - icon: , + icon: , label: "Stock Issue", requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.ADMIN], path: "/stockIssue", }, - //TODO: anna - // { - // icon: , - // label: "Stock Issue", - // path: "/stockIssue", - // }, { - icon: , + icon: , label: "Put Away Scan", requiredAbility: [AUTH.STOCK, AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.ADMIN], path: "/putAway", }, { - icon: , + icon: , label: "Finished Good Order", requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN], path: "/finishedGood", }, { - icon: , + icon: , label: "Stock Record", requiredAbility: [AUTH.STOCK_TAKE, AUTH.STOCK_IN_BIND, AUTH.STOCK_FG, AUTH.ADMIN], path: "/stockRecord", @@ -139,106 +126,44 @@ const NavigationContent: React.FC = () => { ], }, { - icon: , - label: "Delivery", - path: "", + icon: , + label: "Delivery Order", + path: "/do", requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN], - children: [ - { - icon: , - label: "Delivery Order", - requiredAbility: [AUTH.STOCK_FG, AUTH.ADMIN], - path: "/do", - }, - ], }, - // { - // icon: , - // label: "Report", - // path: "", - // children: [ - // { - // icon: , - // label: "report", - // path: "", - // }, - // ], - // }, - // { - // icon: , - // label: "Recipe", - // path: "", - // children: [ - // { - // icon: , - // label: "FG Recipe", - // path: "", - // }, - // { - // icon: , - // label: "SFG Recipe", - // path: "", - // }, - // { - // icon: , - // label: "Recipe", - // path: "", - // }, - // ], - // }, - /* { - icon: , - label: "Scheduling", - path: "", - requiredAbility: [AUTH.FORECAST, AUTH.ADMIN], - children: [ - { - icon: , - label: "Demand Forecast", - path: "/scheduling/rough", - }, - { - icon: , - label: "Detail Scheduling", - path: "/scheduling/detailed", - }, - ], - }, - */ - { - icon: , + icon: , label: "Scheduling", path: "/ps", requiredAbility: [AUTH.FORECAST, AUTH.ADMIN], isHidden: false, }, { - icon: , + icon: , label: "Management Job Order", path: "", requiredAbility: [AUTH.JOB_CREATE, AUTH.JOB_PICK, AUTH.JOB_PROD, AUTH.ADMIN], children: [ { - icon: , + icon: , label: "Search Job Order/ Create Job Order", requiredAbility: [AUTH.JOB_CREATE, AUTH.ADMIN], path: "/jo", }, { - icon: , + icon: , label: "Job Order Pickexcution", requiredAbility: [AUTH.JOB_PICK, AUTH.JOB_MAT, AUTH.ADMIN], path: "/jodetail", }, { - icon: , + icon: , label: "Job Order Production Process", requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN], path: "/productionProcess", }, { - icon: , + icon: , label: "Bag Usage", requiredAbility: [AUTH.JOB_PROD, AUTH.ADMIN], path: "/bag", @@ -246,134 +171,99 @@ const NavigationContent: React.FC = () => { ], }, { - icon: , + icon: , label: "打袋機列印", path: "/testing", requiredAbility: [AUTH.TESTING, AUTH.ADMIN], isHidden: false, }, { - icon: , + icon: , label: "報告管理", path: "/report", requiredAbility: [AUTH.TESTING, AUTH.ADMIN], isHidden: false, }, { - icon: , + icon: , label: "Settings", path: "", requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN], children: [ { - icon: , + icon: , label: "User", path: "/settings/user", requiredAbility: [AUTH.VIEW_USER, AUTH.ADMIN], }, { - icon: , + icon: , label: "User Group", path: "/settings/user", requiredAbility: [AUTH.VIEW_GROUP, AUTH.ADMIN], }, - // { - // icon: , - // label: "Material", - // path: "/settings/material", - // }, - // { - // icon: , - // label: "By-product", - // path: "/settings/byProduct", - // }, { - icon: , + icon: , label: "Items", path: "/settings/items", }, { - icon: , + icon: , label: "ShopAndTruck", path: "/settings/shop", }, { - icon: , + icon: , label: "Demand Forecast Setting", path: "/settings/rss", }, - //{ - // icon: , - // label: "Equipment Type", - // path: "/settings/equipmentType", - //}, { - icon: , + icon: , label: "Equipment", path: "/settings/equipment", }, { - icon: , + icon: , label: "Warehouse", path: "/settings/warehouse", }, { - icon: , + icon: , label: "Printer", path: "/settings/printer", }, - //{ - // icon: , - // label: "Supplier", - // path: "/settings/user", - //}, { - icon: , + icon: , label: "Customer", path: "/settings/user", }, { - icon: , + icon: , label: "QC Check Item", path: "/settings/qcItem", }, { - icon: , + icon: