diff --git a/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx b/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx
index a1751be..771d21f 100644
--- a/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx
+++ b/src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx
@@ -1,24 +1,28 @@
-//src\app\(main)\analytics\ResourceOvercomsumptionReport\page.tsx
import { Metadata } from "next";
-import { I18nProvider } from "@/i18n";
-import Typography from "@mui/material/Typography";
-import ResourceOverconsumptionReportComponent from "@/components/Report/ResourceOverconsumptionReport";
+import { Suspense } from "react";
+import { I18nProvider, getServerI18n } from "@/i18n";
+import ResourceOverconsumptionReport from "@/components/ResourceOverconsumptionReport";
+import { Typography } from "@mui/material";
export const metadata: Metadata = {
- title: "Resource Overconsumption Report",
+ title: "Staff Monthly Work Hours Analysis Report",
};
-const ResourceOverconsumptionReport: React.FC = () => {
- return (
-
-
- Resource Overconsumption Report
-
- {/* }>
-
- */}
-
-
- );
+const StaffMonthlyWorkHoursAnalysisReport: React.FC = async () => {
+ const { t } = await getServerI18n("User Group");
+
+ return (
+ <>
+
+ {t("Project Resource Overconsumption Report")}
+
+
+ }>
+
+
+
+ >
+ );
};
-export default ResourceOverconsumptionReport;
+
+export default StaffMonthlyWorkHoursAnalysisReport;
diff --git a/src/app/api/report3/index.ts b/src/app/api/report3/index.ts
deleted file mode 100644
index b2c8751..0000000
--- a/src/app/api/report3/index.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-//src\app\api\report\index.ts
-import { cache } from "react";
-
-export interface ResourceOverconsumption {
- id: number;
- projectCode: string;
- projectName: string;
- team: string;
- teamLeader: string;
- startDate: string;
- startDateFrom: string;
- startDateTo: string;
- targetEndDate: string;
- client: string;
- subsidiary: string;
- status: string;
-}
-
-export const preloadProjects = () => {
- fetchProjectsResourceOverconsumption();
-};
-
-export const fetchProjectsResourceOverconsumption = cache(async () => {
- return mockProjects;
-});
-
-const mockProjects: ResourceOverconsumption[] = [
- {
- id: 1,
- projectCode: "CUST-001",
- projectName: "Client A",
- team: "N/A",
- teamLeader: "N/A",
- startDate: "1/2/2024",
- startDateFrom: "1/2/2024",
- startDateTo: "1/2/2024",
- targetEndDate: "30/3/2024",
- client: "ss",
- subsidiary: "sus",
- status: "1",
- },
-];
diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts
index 3d49a16..a4c2b85 100644
--- a/src/app/api/reports/actions.ts
+++ b/src/app/api/reports/actions.ts
@@ -1,7 +1,7 @@
"use server";
import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil";
-import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest } from ".";
+import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest, ProjectResourceOverconsumptionReportRequest } from ".";
import { BASE_API_URL } from "@/config/api";
export interface FileResponse {
@@ -35,6 +35,19 @@ export const fetchMonthlyWorkHoursReport = async (data: MonthlyWorkHoursReportRe
return reportBlob
};
+export const fetchProjectResourceOverconsumptionReport = async (data: ProjectResourceOverconsumptionReportRequest) => {
+ const reportBlob = await serverFetchBlob(
+ `${BASE_API_URL}/reports/ProjectResourceOverconsumptionReport`,
+ {
+ method: "POST",
+ body: JSON.stringify(data),
+ headers: { "Content-Type": "application/json" },
+ },
+ );
+
+ return reportBlob
+};
+
// export const fetchLateStartReport = async (data: LateStartReportRequest) => {
// const response = await serverFetchBlob(
// `${BASE_API_URL}/reports/downloadLateStartReport`,
diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts
index dd1afb8..f1fa613 100644
--- a/src/app/api/reports/index.ts
+++ b/src/app/api/reports/index.ts
@@ -25,6 +25,20 @@ export interface MonthlyWorkHoursReportRequest {
id: number;
yearMonth: string;
}
+// - Project Resource Overconsumption Report
+export interface ProjectResourceOverconsumptionReportFilter {
+ team: string[];
+ customer: string[];
+ status: string[];
+ lowerLimit: number;
+}
+
+export interface ProjectResourceOverconsumptionReportRequest {
+ teamId?: number
+ custId?: number
+ status: "All" | "Within Budget" | "Potential Overconsumption" | "Overconsumption"
+ lowerLimit: number
+}
export interface LateStartReportFilter {
remainedDays: number;
diff --git a/src/components/Report/ReportSearchBox3/SearchBox3.tsx b/src/components/Report/ReportSearchBox3/SearchBox3.tsx
deleted file mode 100644
index aafa7d0..0000000
--- a/src/components/Report/ReportSearchBox3/SearchBox3.tsx
+++ /dev/null
@@ -1,313 +0,0 @@
-//src\components\ReportSearchBox3\SearchBox3.tsx
-"use client";
-
-import Grid from "@mui/material/Grid";
-import Card from "@mui/material/Card";
-import CardContent from "@mui/material/CardContent";
-import Typography from "@mui/material/Typography";
-import React, { useCallback, useMemo, useState } from "react";
-import { useTranslation } from "react-i18next";
-import TextField from "@mui/material/TextField";
-import FormControl from "@mui/material/FormControl";
-import InputLabel from "@mui/material/InputLabel";
-import Select, { SelectChangeEvent } from "@mui/material/Select";
-import MenuItem from "@mui/material/MenuItem";
-import CardActions from "@mui/material/CardActions";
-import Button from "@mui/material/Button";
-import RestartAlt from "@mui/icons-material/RestartAlt";
-import Search from "@mui/icons-material/Search";
-import dayjs from "dayjs";
-import "dayjs/locale/zh-hk";
-import { DatePicker } from "@mui/x-date-pickers/DatePicker";
-import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
-import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
-import { Box } from "@mui/material";
-import * as XLSX from 'xlsx-js-style';
-//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton';
-
-interface BaseCriterion {
- label: string;
- label2?: string;
- paramName: T;
- paramName2?: T;
-}
-
-interface TextCriterion extends BaseCriterion {
- type: "text";
-}
-
-interface SelectCriterion extends BaseCriterion {
- type: "select";
- options: string[];
-}
-
-interface DateRangeCriterion extends BaseCriterion {
- type: "dateRange";
-}
-
-export type Criterion =
- | TextCriterion
- | SelectCriterion
- | DateRangeCriterion;
-
-interface Props {
- criteria: Criterion[];
- onSearch: (inputs: Record) => void;
- onReset?: () => void;
-}
-
-function SearchBox({
- criteria,
- onSearch,
- onReset,
-}: Props) {
- const { t } = useTranslation("common");
- const defaultInputs = useMemo(
- () =>
- criteria.reduce>(
- (acc, c) => {
- return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
- },
- {} as Record,
- ),
- [criteria],
- );
- const [inputs, setInputs] = useState(defaultInputs);
-
- const makeInputChangeHandler = useCallback(
- (paramName: T): React.ChangeEventHandler => {
- return (e) => {
- setInputs((i) => ({ ...i, [paramName]: e.target.value }));
- };
- },
- [],
- );
-
- const makeSelectChangeHandler = useCallback((paramName: T) => {
- return (e: SelectChangeEvent) => {
- setInputs((i) => ({ ...i, [paramName]: e.target.value }));
- };
- }, []);
-
- const makeDateChangeHandler = useCallback((paramName: T) => {
- return (e: any) => {
- setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM-DD") }));
- };
- }, []);
-
- const makeDateToChangeHandler = useCallback((paramName: T) => {
- return (e: any) => {
- setInputs((i) => ({
- ...i,
- [paramName + "To"]: dayjs(e).format("YYYY-MM-DD"),
- }));
- };
- }, []);
-
- const handleReset = () => {
- setInputs(defaultInputs);
- onReset?.();
- };
-
- const handleSearch = () => {
- onSearch(inputs);
-
- };
-
- const handleDownload = async () => {
- //setIsLoading(true);
-
- try {
- const response = await fetch('/temp/AR03_Resource Overconsumption.xlsx', {
- headers: {
- 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
- },
- });
- if (!response.ok) throw new Error('Network response was not ok.');
-
- const data = await response.blob();
- const reader = new FileReader();
- reader.onload = (e) => {
- if (e.target && e.target.result) {
- const ab = e.target.result as ArrayBuffer;
- const workbook = XLSX.read(ab, { type: 'array' });
- const firstSheetName = workbook.SheetNames[0];
- const worksheet = workbook.Sheets[firstSheetName];
-
- // Add the current date to cell C2
- const cellAddress = 'C2';
- const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD
- const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD
- XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress });
-
- // Style for cell A1: Font size 16 and bold
- if (worksheet['A1']) {
- worksheet['A1'].s = {
- font: {
- bold: true,
- sz: 16, // Font size 16
- //name: 'Times New Roman' // Specify font
- }
- };
- }
-
- // Apply styles from A2 to A4 (bold)
- ['A2', 'A3', 'A4'].forEach(cell => {
- if (worksheet[cell]) {
- worksheet[cell].s = { font: { bold: true } };
- }
- });
-
- // Formatting from A6 to L6
- // Apply styles from A6 to L6 (bold, bottom border, center alignment)
- for (let col = 0; col < 12; col++) { // Columns A to K
- const cellRef = XLSX.utils.encode_col(col) + '6';
- if (worksheet[cellRef]) {
- worksheet[cellRef].s = {
- font: { bold: true },
- alignment: { horizontal: 'center' },
- border: {
- bottom: { style: 'thin', color: { auto: 1 } }
- }
- };
- }
- }
-
- const firstTableData = [
- ['Column1', 'Column2', 'Column3'], // Row 1
- ['Data1', 'Data2', 'Data3'], // Row 2
- // ... more rows as needed
- ];
- // Find the last row of the first table
- let lastRowOfFirstTable = 6; // Starting row for data in the first table
- while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) {
- lastRowOfFirstTable++;
- }
-
- // Calculate the maximum length of content in each column and set column width
- const colWidths: number[] = [];
-
- const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][];
- jsonData.forEach((row: (string | number)[]) => {
- row.forEach((cell: string | number, index: number) => {
- const valueLength = cell.toString().length;
- colWidths[index] = Math.max(colWidths[index] || 0, valueLength);
- });
- });
-
- // Apply calculated widths to each column, skipping column A
- worksheet['!cols'] = colWidths.map((width, index) => {
- if (index === 0) {
- return { wch: 8 }; // Set default or specific width for column A if needed
- }
- return { wch: width + 2 }; // Add padding to width
- });
-
- // Format filename with date
- const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD
- const filename = `AR03_Resource_Overconsumption_${today}.xlsx`; // Append formatted date to the filename
-
- // Convert workbook back to XLSX file
- XLSX.writeFile(workbook, filename);
- } else {
- throw new Error('Failed to load file');
- }
- };
- reader.readAsArrayBuffer(data);
- } catch (error) {
- console.error('Error downloading the file: ', error);
- }
-
- //setIsLoading(false);
- };
- return (
-
-
- {t("Search Criteria")}
-
- {criteria.map((c) => {
- return (
-
- {c.type === "text" && (
-
- )}
- {c.type === "select" && (
-
- {c.label}
-
-
- )}
- {c.type === "dateRange" && (
-
-
-
-
-
-
- {"-"}
-
-
-
-
-
-
- )}
-
- );
- })}
-
-
- }
- onClick={handleReset}
- >
- {t("Reset")}
-
- }
- onClick={handleDownload}
- >
- {t("Download")}
-
-
-
-
- );
-}
-
-export default SearchBox;
diff --git a/src/components/Report/ReportSearchBox3/index.ts b/src/components/Report/ReportSearchBox3/index.ts
deleted file mode 100644
index d481fbd..0000000
--- a/src/components/Report/ReportSearchBox3/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-//src\components\SearchBox\index.ts
-export { default } from "./SearchBox3";
-export type { Criterion } from "./SearchBox3";
diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx b/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx
deleted file mode 100644
index a6ec216..0000000
--- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-//src\components\LateStartReportGen\LateStartReportGen.tsx
-"use client";
-import React, { useMemo, useState } from "react";
-import SearchBox, { Criterion } from "../ReportSearchBox3";
-import { useTranslation } from "react-i18next";
-import { ResourceOverconsumption } from "@/app/api/report3";
-
-interface Props {
- projects: ResourceOverconsumption[];
-}
-type SearchQuery = Partial>;
-type SearchParamNames = keyof SearchQuery;
-
-const ProgressByClientSearch: React.FC = ({ projects }) => {
- const { t } = useTranslation("projects");
-
- const searchCriteria: Criterion[] = useMemo(
- () => [
- { label: "Team", paramName: "team", type: "select", options: ["AAA", "BBB", "CCC"] },
- { label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] },
- { label: "Status", paramName: "status", type: "select", options: ["Overconsumption", "Potential Overconsumption"] },
- // {
- // label: "Status",
- // label2: "Remained Date To",
- // paramName: "targetEndDate",
- // type: "dateRange",
- // },
- ],
- [t],
- );
-
- return (
- <>
- {
- console.log(query);
- }}
- />
- {/* */}
- >
- );
-};
-
-export default ProgressByClientSearch;
diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx b/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx
deleted file mode 100644
index a93f64b..0000000
--- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-//src\components\LateStartReportGen\LateStartReportGenWrapper.tsx
-import { fetchProjectsResourceOverconsumption } from "@/app/api/report3";
-import React from "react";
-import ResourceOvercomsumptionReportGen from "./ResourceOverconsumptionReportGen";
-import ResourceOvercomsumptionReportGenLoading from "./ResourceOverconsumptionReportGenLoading";
-
-interface SubComponents {
- Loading: typeof ResourceOvercomsumptionReportGenLoading;
-}
-
-const ResourceOvercomsumptionReportGenWrapper: React.FC & SubComponents = async () => {
- const clentprojects = await fetchProjectsResourceOverconsumption();
-
- return ;
-};
-
-ResourceOvercomsumptionReportGenWrapper.Loading = ResourceOvercomsumptionReportGenLoading;
-
-export default ResourceOvercomsumptionReportGenWrapper;
\ No newline at end of file
diff --git a/src/components/Report/ResourceOverconsumptionReportGen/index.ts b/src/components/Report/ResourceOverconsumptionReportGen/index.ts
deleted file mode 100644
index 82fb633..0000000
--- a/src/components/Report/ResourceOverconsumptionReportGen/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-//src\components\DelayReportGen\index.ts
-export { default } from "./ResourceOverconsumptionReportGenWrapper";
diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx
new file mode 100644
index 0000000..5e6e76a
--- /dev/null
+++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx
@@ -0,0 +1,96 @@
+"use client";
+import React, { useMemo } from "react";
+import SearchBox, { Criterion } from "../SearchBox";
+import { useTranslation } from "react-i18next";
+import { ProjectResult } from "@/app/api/projects";
+import { fetchMonthlyWorkHoursReport, fetchProjectCashFlowReport, fetchProjectResourceOverconsumptionReport } from "@/app/api/reports/actions";
+import { downloadFile } from "@/app/utils/commonUtil";
+import { BASE_API_URL } from "@/config/api";
+import { ProjectResourceOverconsumptionReportFilter, ProjectResourceOverconsumptionReportRequest } from "@/app/api/reports";
+import { StaffResult } from "@/app/api/staff";
+import { TeamResult } from "@/app/api/team";
+import { Customer } from "@/app/api/customer";
+
+interface Props {
+ team: TeamResult[]
+ customer: Customer[]
+}
+
+type SearchQuery = Partial>;
+type SearchParamNames = keyof SearchQuery;
+
+const ResourceOverconsumptionReport: React.FC = ({ team, customer }) => {
+ const { t } = useTranslation();
+ const teamCombo = team.map(t => `${t.name} - ${t.code}`)
+ const custCombo = customer.map(c => `${c.name} - ${c.code}`)
+ const statusCombo = ["Within Budget, Overconsumption", "Potential Overconsumption"]
+ // const staffCombo = staffs.map(staff => `${staff.name} - ${staff.staffId}`)
+ // console.log(staffs)
+
+ const searchCriteria: Criterion[] = useMemo(
+ () => [
+ {
+ label: t("Team"),
+ paramName: "team",
+ type: "select",
+ options: teamCombo,
+ needAll: true
+ },
+ {
+ label: t("Client"),
+ paramName: "customer",
+ type: "select",
+ options: custCombo,
+ needAll: true
+ },
+ {
+ label: t("Status"),
+ paramName: "status",
+ type: "select",
+ options: statusCombo,
+ needAll: true
+ },
+ {
+ label: t("lowerLimit"),
+ paramName: "lowerLimit",
+ type: "number",
+ },
+ ],
+ [t],
+ );
+
+return (
+ <>
+ {
+ let index = 0
+ let postData: ProjectResourceOverconsumptionReportRequest = {
+ status: "All",
+ lowerLimit: 0.9
+ }
+ if (query.team.length > 0 && query.team.toLocaleLowerCase() !== "all") {
+ index = teamCombo.findIndex(team => team === query.team)
+ postData.teamId = team[index].id
+ }
+ if (query.customer.length > 0 && query.customer.toLocaleLowerCase() !== "all") {
+ index = custCombo.findIndex(customer => customer === query.customer)
+ postData.custId = customer[index].id
+ }
+ if (Boolean(query.lowerLimit)) {
+ postData.lowerLimit = query.lowerLimit/100
+ }
+ postData.status = query.status
+ console.log(postData)
+ const response = await fetchProjectResourceOverconsumptionReport(postData)
+ if (response) {
+ downloadFile(new Uint8Array(response.blobValue), response.filename!!)
+ }
+ }
+ }
+ />
+>
+ )
+}
+
+export default ResourceOverconsumptionReport
\ No newline at end of file
diff --git a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx
similarity index 89%
rename from src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx
rename to src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx
index 9ae7417..945d13c 100644
--- a/src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx
+++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx
@@ -6,7 +6,7 @@ import Stack from "@mui/material/Stack";
import React from "react";
// Can make this nicer
-export const ResourceOvercomsumptionReportGenLoading: React.FC = () => {
+export const ResourceOvercomsumptionReportLoading: React.FC = () => {
return (
<>
@@ -38,4 +38,4 @@ export const ResourceOvercomsumptionReportGenLoading: React.FC = () => {
);
};
-export default ResourceOvercomsumptionReportGenLoading;
+export default ResourceOvercomsumptionReportLoading;
diff --git a/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx
new file mode 100644
index 0000000..1ab9d24
--- /dev/null
+++ b/src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+import ResourceOvercomsumptionReportLoading from "./ResourceOverconsumptionReportLoading";
+import ResourceOverconsumptionReport from "./ResourceOverconsumptionReport";
+import { fetchAllCustomers } from "@/app/api/customer";
+import { fetchTeam } from "@/app/api/team";
+
+interface SubComponents {
+ Loading: typeof ResourceOvercomsumptionReportLoading;
+}
+
+const ResourceOvercomsumptionReportWrapper: React.FC & SubComponents = async () => {
+ const customers = await fetchAllCustomers()
+ const teams = await fetchTeam ()
+
+ return ;
+};
+
+ResourceOvercomsumptionReportWrapper.Loading = ResourceOvercomsumptionReportLoading;
+
+export default ResourceOvercomsumptionReportWrapper;
\ No newline at end of file
diff --git a/src/components/ResourceOverconsumptionReport/index.ts b/src/components/ResourceOverconsumptionReport/index.ts
new file mode 100644
index 0000000..b5f20e2
--- /dev/null
+++ b/src/components/ResourceOverconsumptionReport/index.ts
@@ -0,0 +1 @@
+export { default } from "./ResourceOverconsumptionReportWrapper";
\ No newline at end of file
diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx
index 686eb8d..2ab52b0 100644
--- a/src/components/SearchBox/SearchBox.tsx
+++ b/src/components/SearchBox/SearchBox.tsx
@@ -4,7 +4,7 @@ import Grid from "@mui/material/Grid";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Typography from "@mui/material/Typography";
-import React, { useCallback, useMemo, useState } from "react";
+import React, { FocusEvent, KeyboardEvent, PointerEvent, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import TextField from "@mui/material/TextField";
import FormControl from "@mui/material/FormControl";
@@ -15,7 +15,7 @@ import CardActions from "@mui/material/CardActions";
import Button from "@mui/material/Button";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Search from "@mui/icons-material/Search";
-import FileDownload from '@mui/icons-material/FileDownload';
+import FileDownload from "@mui/icons-material/FileDownload";
import dayjs from "dayjs";
import "dayjs/locale/zh-hk";
import { DatePicker } from "@mui/x-date-pickers/DatePicker";
@@ -23,6 +23,17 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { Box } from "@mui/material";
import { DateCalendar } from "@mui/x-date-pickers";
+import {
+ Unstable_NumberInput as BaseNumberInput,
+ NumberInputProps,
+ numberInputClasses,
+} from "@mui/base/Unstable_NumberInput";
+import {
+ StyledButton,
+ StyledInputElement,
+ StyledInputRoot,
+} from "@/theme/colorConst";
+import { InputAdornment, NumberInput } from "../utils/numberInput";
interface BaseCriterion {
label: string;
@@ -49,17 +60,22 @@ interface MonthYearCriterion extends BaseCriterion {
type: "monthYear";
}
+interface NumberCriterion extends BaseCriterion {
+ type: "number";
+}
+
export type Criterion =
| TextCriterion
| SelectCriterion
| DateRangeCriterion
- | MonthYearCriterion;
+ | MonthYearCriterion
+ | NumberCriterion;
interface Props {
criteria: Criterion[];
onSearch: (inputs: Record) => void;
onReset?: () => void;
- formType?: String,
+ formType?: String;
}
function SearchBox({
@@ -75,10 +91,14 @@ function SearchBox({
(acc, c) => {
return {
...acc,
- [c.paramName]: c.type === "select" ?
- !(c.needAll === false) ? "All" :
- c.options.length > 0 ? c.options[0] : ""
- : ""
+ [c.paramName]:
+ c.type === "select"
+ ? !(c.needAll === false)
+ ? "All"
+ : c.options.length > 0
+ ? c.options[0]
+ : ""
+ : "",
};
},
{} as Record
@@ -86,7 +106,7 @@ function SearchBox({
[criteria]
);
const [inputs, setInputs] = useState(defaultInputs);
-
+
const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler => {
return (e) => {
@@ -95,6 +115,15 @@ function SearchBox({
},
[]
);
+
+ const makeNumberChangeHandler = useCallback(
+ (paramName: T): (event: FocusEvent | PointerEvent | KeyboardEvent, value: number | null) => void => {
+ return (event, value) => {
+ setInputs((i) => ({ ...i, [paramName]: value }));
+ };
+ },
+ []
+ );
const makeSelectChangeHandler = useCallback((paramName: T) => {
return (e: SelectChangeEvent) => {
@@ -110,7 +139,7 @@ function SearchBox({
const makeMonthYearChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
- console.log(dayjs(e).format("YYYY-MM"))
+ console.log(dayjs(e).format("YYYY-MM"));
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") }));
};
}, []);
@@ -168,6 +197,15 @@ function SearchBox({
)}
+ {c.type === "number" && (
+ %}
+ />
+ )}
{c.type === "monthYear" && (
({
) || }
+ startIcon={
+ (formType === "download" && ) ||
+ }
onClick={handleSearch}
>
{(formType === "download" && t("Download")) || t("Search")}
diff --git a/src/components/utils/numberInput.tsx b/src/components/utils/numberInput.tsx
new file mode 100644
index 0000000..2ccaa6b
--- /dev/null
+++ b/src/components/utils/numberInput.tsx
@@ -0,0 +1,192 @@
+import * as React from 'react';
+import {
+ Unstable_NumberInput as BaseNumberInput,
+ NumberInputProps,
+ numberInputClasses,
+} from '@mui/base/Unstable_NumberInput';
+import { styled } from '@mui/system';
+// FocusEvent | PointerEvent | KeyboardEvent
+export const NumberInput = React.forwardRef(function CustomNumberInput(
+ props: NumberInputProps,
+ ref: React.ForwardedRef,
+) {
+ return (
+
+ );
+});
+export const InputAdornment = styled('div')(
+ ({ theme }) => `
+ margin: 8px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ grid-row: 1/3;
+ color: ${theme.palette.mode === 'dark' ? grey[500] : grey[700]};
+ `,
+ );
+
+export default function NumberInputBasic() {
+ const [value, setValue] = React.useState(null);
+ return (
+ setValue(val)}
+ />
+ );
+}
+
+const blue = {
+ 100: '#DAECFF',
+ 200: '#80BFFF',
+ 400: '#3399FF',
+ 500: '#007FFF',
+ 600: '#0072E5',
+};
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const StyledInputRoot = styled('div')(
+ ({ theme }) => `
+ font-family: 'IBM Plex Sans', sans-serif;
+ font-weight: 400;
+ border-radius: 8px;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ display: grid;
+ grid-template-columns: 1fr 19px;
+ grid-template-rows: 1fr 1fr;
+ overflow: hidden;
+ column-gap: 8px;
+ padding: 4px;
+
+ &.${numberInputClasses.focused} {
+ border-color: ${blue[400]};
+ box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]};
+ }
+
+ &:hover {
+ border-color: ${blue[400]};
+ }
+
+ // firefox
+ &:focus-visible {
+ outline: 0;
+ }
+`,
+);
+
+const StyledInputElement = styled('input')(
+ ({ theme }) => `
+ font-size: 0.875rem;
+ font-family: inherit;
+ font-weight: 400;
+ line-height: 1.5;
+ grid-column: 1/2;
+ grid-row: 1/3;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ background: inherit;
+ border: none;
+ border-radius: inherit;
+ padding: 8px 12px;
+ outline: 0;
+`,
+);
+
+const StyledButton = styled('button')(
+ ({ theme }) => `
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+ appearance: none;
+ padding: 0;
+ width: 19px;
+ height: 19px;
+ font-family: system-ui, sans-serif;
+ font-size: 0.875rem;
+ line-height: 1;
+ box-sizing: border-box;
+ background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
+ border: 0;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 120ms;
+
+ &:hover {
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]};
+ border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]};
+ cursor: pointer;
+ }
+
+ &.${numberInputClasses.incrementButton} {
+ grid-column: 2/3;
+ grid-row: 1/2;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ border: 1px solid;
+ border-bottom: 0;
+ &:hover {
+ cursor: pointer;
+ background: ${blue[400]};
+ color: ${grey[50]};
+ }
+
+ border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]};
+ }
+
+ &.${numberInputClasses.decrementButton} {
+ grid-column: 2/3;
+ grid-row: 2/3;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border: 1px solid;
+ &:hover {
+ cursor: pointer;
+ background: ${blue[400]};
+ color: ${grey[50]};
+ }
+
+ border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]};
+ }
+ & .arrow {
+ transform: translateY(-1px);
+ }
+`,
+);
\ No newline at end of file
diff --git a/src/theme/colorConst.js b/src/theme/colorConst.js
index 0db2053..8055843 100644
--- a/src/theme/colorConst.js
+++ b/src/theme/colorConst.js
@@ -1,5 +1,11 @@
import { createTheme } from "@mui/material";
import { aborted } from "util";
+import { styled } from '@mui/system';
+import {
+ Unstable_NumberInput as BaseNumberInput,
+ NumberInputProps,
+ numberInputClasses,
+} from '@mui/base/Unstable_NumberInput';
// - - - - - - WORK IN PROGRESS - - - - - - //
@@ -415,3 +421,150 @@ export const TSMS_LONG_BUTTON_THEME = createTheme({
},
},
});
+
+export default function NumberInputBasic() {
+ const [value, setValue] = React.useState(null);
+ return (
+ setValue(val)}
+ />
+ );
+}
+const blue = {
+ 100: '#DAECFF',
+ 200: '#80BFFF',
+ 400: '#3399FF',
+ 500: '#007FFF',
+ 600: '#0072E5',
+};
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+export const StyledInputRoot = styled('div')(
+ ({ theme }) => `
+ font-family: 'IBM Plex Sans', sans-serif;
+ font-weight: 400;
+ border-radius: 8px;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
+ border: 1px solid ${theme.palette.mode === 'dark' ? grey[700] : grey[200]};
+ box-shadow: 0px 2px 2px ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ display: grid;
+ grid-template-columns: 1fr 19px;
+ grid-template-rows: 1fr 1fr;
+ overflow: hidden;
+ column-gap: 8px;
+ padding: 4px;
+
+ &.${numberInputClasses.focused} {
+ border-color: ${blue[400]};
+ box-shadow: 0 0 0 3px ${theme.palette.mode === 'dark' ? blue[600] : blue[200]};
+ }
+
+ &:hover {
+ border-color: ${blue[400]};
+ }
+
+ // firefox
+ &:focus-visible {
+ outline: 0;
+ }
+`,
+);
+
+export const StyledInputElement = styled('input')(
+ ({ theme }) => `
+ font-size: 0.875rem;
+ font-family: inherit;
+ font-weight: 400;
+ line-height: 1.5;
+ grid-column: 1/2;
+ grid-row: 1/3;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ background: inherit;
+ border: none;
+ border-radius: inherit;
+ padding: 8px 12px;
+ outline: 0;
+`,
+);
+
+export const StyledButton = styled('button')(
+ ({ theme }) => `
+ display: flex;
+ flex-flow: row nowrap;
+ justify-content: center;
+ align-items: center;
+ appearance: none;
+ padding: 0;
+ width: 19px;
+ height: 19px;
+ font-family: system-ui, sans-serif;
+ font-size: 0.875rem;
+ line-height: 1;
+ box-sizing: border-box;
+ background: ${theme.palette.mode === 'dark' ? grey[900] : '#fff'};
+ border: 0;
+ color: ${theme.palette.mode === 'dark' ? grey[300] : grey[900]};
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 120ms;
+
+ &:hover {
+ background: ${theme.palette.mode === 'dark' ? grey[800] : grey[50]};
+ border-color: ${theme.palette.mode === 'dark' ? grey[600] : grey[300]};
+ cursor: pointer;
+ }
+
+ &.${numberInputClasses.incrementButton} {
+ grid-column: 2/3;
+ grid-row: 1/2;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ border: 1px solid;
+ border-bottom: 0;
+ &:hover {
+ cursor: pointer;
+ background: ${blue[400]};
+ color: ${grey[50]};
+ }
+
+ border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]};
+ }
+
+ &.${numberInputClasses.decrementButton} {
+ grid-column: 2/3;
+ grid-row: 2/3;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border: 1px solid;
+ &:hover {
+ cursor: pointer;
+ background: ${blue[400]};
+ color: ${grey[50]};
+ }
+
+ border-color: ${theme.palette.mode === 'dark' ? grey[800] : grey[200]};
+ background: ${theme.palette.mode === 'dark' ? grey[900] : grey[50]};
+ color: ${theme.palette.mode === 'dark' ? grey[200] : grey[900]};
+ }
+ & .arrow {
+ transform: translateY(-1px);
+ }
+`,
+);
\ No newline at end of file