From f0ddd56381a1d2da33b7fe9dc3a54dbb0359ea50 Mon Sep 17 00:00:00 2001 From: "vluk@2fi-solutions.com.hk" Date: Mon, 23 Feb 2026 14:28:05 +0800 Subject: [PATCH] changed the look and feel slightly --- .cursor/rules.md | 91 +++ src/app/(main)/do/edit/page.tsx | 40 +- src/app/(main)/do/page.tsx | 10 +- src/app/(main)/jo/edit/page.tsx | 69 +- src/app/(main)/jo/page.tsx | 45 +- src/app/(main)/jodetail/edit/page.tsx | 61 +- src/app/(main)/jodetail/page.tsx | 51 +- src/app/(main)/layout.tsx | 2 +- src/app/(main)/ps/page.tsx | 760 ++++++++++-------- src/app/global.css | 51 +- src/components/AppBar/AppBar.tsx | 20 +- src/components/AppBar/NavigationToggle.tsx | 7 +- src/components/AppBar/Profile.tsx | 23 +- src/components/DoSearch/DoSearch.tsx | 86 +- src/components/Logo/Logo.tsx | 99 ++- .../NavigationContent/NavigationContent.tsx | 353 ++++---- src/components/PageTitleBar/PageTitleBar.tsx | 35 + src/components/PageTitleBar/index.ts | 1 + src/components/SearchBox/SearchBox.tsx | 14 +- .../SearchResults/SearchResults.tsx | 2 +- .../StyledDataGrid/StyledDataGrid.tsx | 23 +- src/theme/devias-material-kit/colors.ts | 10 +- src/theme/devias-material-kit/components.ts | 101 ++- src/theme/devias-material-kit/palette.ts | 2 +- tailwind.config.js | 15 +- 25 files changed, 1103 insertions(+), 868 deletions(-) create mode 100644 .cursor/rules.md create mode 100644 src/components/PageTitleBar/PageTitleBar.tsx create mode 100644 src/components/PageTitleBar/index.ts 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 857f3b1..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 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 b008657..de32437 100644 --- a/src/app/(main)/ps/page.tsx +++ b/src/app/(main)/ps/page.tsx @@ -1,85 +1,65 @@ "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, + Eye, + ListOrdered, + LineChart, + Download, + Network, + Loader2, +} from "lucide-react"; +import PageTitleBar from "@/components/PageTitleBar"; import dayjs from "dayjs"; 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 () => { setLoading(true); - try { - const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/ps/search-ps?produceAt=${searchDate}`, { - method: 'GET', - }); - + 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); @@ -89,28 +69,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; } - 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 clientAuthFetch(url, { method: 'GET' }); - + 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(); @@ -130,27 +104,21 @@ export default function ProductionSchedulePage() { alert("Please select a from date."); return; } - setLoading(true); setIsExportDialogOpen(false); - try { - const params = new URLSearchParams({ - fromDate: exportFromDate, - }); - - const response = await clientAuthFetch(`${NEXT_PUBLIC_API_URL}/productionSchedule/export-prod-schedule?${params.toString()}`, { - method: 'GET', - }); - + 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); @@ -164,41 +132,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; } - setSelectedPs(ps); setLoading(true); - try { const url = `${NEXT_PUBLIC_API_URL}/ps/search-ps-line?psId=${ps.id}`; - - const response = await clientAuthFetch(url, { method: 'GET' }); - + 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"); @@ -207,25 +158,21 @@ export default function ProductionSchedulePage() { } }; - const handleAutoGenJob = async () => { - //if (!isDateToday) return; setIsGenerating(true); try { - 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 }) - }); - + 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."); @@ -238,263 +185,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 */} + {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)} + /> +
+

+ 準備生成預計排期 +

+
+
+
+
+ + { + const val = + e.target.value === "" ? "" : Number(e.target.value); + if ( + val === "" || + (Number.isInteger(val) && val >= 1 && val <= 365) + ) { + setForecastDays(val); + } + }} + 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/global.css b/src/app/global.css index 7d2ff9a..261f31c 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -1,7 +1,52 @@ - +@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 +} + +body { + background-color: var(--background); + color: var(--foreground); +} + +.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/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/DoSearch/DoSearch.tsx b/src/components/DoSearch/DoSearch.tsx index f1c570a..278f97b 100644 --- a/src/components/DoSearch/DoSearch.tsx +++ b/src/components/DoSearch/DoSearch.tsx @@ -27,7 +27,7 @@ import { SubmitHandler, useForm, } from "react-hook-form"; -import { Box, Button, Grid, Stack, Typography, TablePagination} from "@mui/material"; +import { Box, Button, Paper, Stack, Typography, TablePagination } from "@mui/material"; import StyledDataGrid from "../StyledDataGrid"; import { GridRowSelectionModel } from "@mui/x-data-grid"; import Swal from "sweetalert2"; @@ -605,32 +605,17 @@ const handleSearch = useCallback(async (query: SearchBoxInputs) => { component="form" onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} > - - - - {t("Delivery Order")} - - - - - {hasSearched && hasResults && ( - - )} + {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/Logo/Logo.tsx b/src/components/Logo/Logo.tsx index de2ea41..8e18be2 100644 --- a/src/components/Logo/Logo.tsx +++ b/src/components/Logo/Logo.tsx @@ -1,32 +1,89 @@ +"use client"; + interface Props { width?: number; height?: number; + className?: string; } -const Logo: React.FC = ({ width, height }) => { +/** + * Logo: 3D-style badge (FP) + MTMS wordmark. + * Badge uses gradient and highlight for depth; FP = Food Production, MTMS = system name. + */ +const Logo: React.FC = ({ height = 44, className = "" }) => { + const size = Math.max(28, height); + const badgeSize = Math.round(size * 0.7); + const fontSize = Math.round(size * 0.5); + const fpSize = badgeSize <= 22 ? 10 : badgeSize <= 28 ? 12 : 14; + return ( - - - - - + + {/* Energetic blue gradient: bright top → deep blue bottom */} + + + + + + + + + + + + + + {/* Shadow layer - deep blue */} + + {/* Main 3D body */} + + {/* Top bevel (inner 3D) */} + + {/* FP text */} + + FP + + + {/* Wordmark: MTMS + subtitle — strong, energetic */} +
+ + 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: