Kaynağa Gözat

update overconsumption report

tags/Baseline_30082024_FRONTEND_UAT
MSI\derek 1 yıl önce
ebeveyn
işleme
afc013a306
16 değiştirilmiş dosya ile 565 ekleme ve 456 silme
  1. +22
    -18
      src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx
  2. +0
    -42
      src/app/api/report3/index.ts
  3. +14
    -1
      src/app/api/reports/actions.ts
  4. +14
    -0
      src/app/api/reports/index.ts
  5. +0
    -313
      src/components/Report/ReportSearchBox3/SearchBox3.tsx
  6. +0
    -3
      src/components/Report/ReportSearchBox3/index.ts
  7. +0
    -45
      src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx
  8. +0
    -19
      src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx
  9. +0
    -2
      src/components/Report/ResourceOverconsumptionReportGen/index.ts
  10. +96
    -0
      src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx
  11. +2
    -2
      src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx
  12. +20
    -0
      src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx
  13. +1
    -0
      src/components/ResourceOverconsumptionReport/index.ts
  14. +51
    -11
      src/components/SearchBox/SearchBox.tsx
  15. +192
    -0
      src/components/utils/numberInput.tsx
  16. +153
    -0
      src/theme/colorConst.js

+ 22
- 18
src/app/(main)/analytics/ResourceOverconsumptionReport/page.tsx Dosyayı Görüntüle

@@ -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 (
<I18nProvider namespaces={["analytics"]}>
<Typography variant="h4" marginInlineEnd={2}>
Resource Overconsumption Report
</Typography>
{/* <Suspense fallback={<ProgressCashFlowSearch.Loading />}>
<ProgressCashFlowSearch/>
</Suspense> */}
<ResourceOverconsumptionReportComponent />
</I18nProvider>
);
const StaffMonthlyWorkHoursAnalysisReport: React.FC = async () => {
const { t } = await getServerI18n("User Group");

return (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Project Resource Overconsumption Report")}
</Typography>
<I18nProvider namespaces={["report", "common"]}>
<Suspense fallback={<ResourceOverconsumptionReport.Loading />}>
<ResourceOverconsumptionReport />
</Suspense>
</I18nProvider>
</>
);
};
export default ResourceOverconsumptionReport;

export default StaffMonthlyWorkHoursAnalysisReport;

+ 0
- 42
src/app/api/report3/index.ts Dosyayı Görüntüle

@@ -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",
},
];

+ 14
- 1
src/app/api/reports/actions.ts Dosyayı Görüntüle

@@ -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<FileResponse>(
`${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<FileResponse>(
// `${BASE_API_URL}/reports/downloadLateStartReport`,


+ 14
- 0
src/app/api/reports/index.ts Dosyayı Görüntüle

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


+ 0
- 313
src/components/Report/ReportSearchBox3/SearchBox3.tsx Dosyayı Görüntüle

@@ -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<T extends string> {
label: string;
label2?: string;
paramName: T;
paramName2?: T;
}

interface TextCriterion<T extends string> extends BaseCriterion<T> {
type: "text";
}

interface SelectCriterion<T extends string> extends BaseCriterion<T> {
type: "select";
options: string[];
}

interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
onSearch: (inputs: Record<T, string>) => void;
onReset?: () => void;
}

function SearchBox<T extends string>({
criteria,
onSearch,
onReset,
}: Props<T>) {
const { t } = useTranslation("common");
const defaultInputs = useMemo(
() =>
criteria.reduce<Record<T, string>>(
(acc, c) => {
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
},
{} as Record<T, string>,
),
[criteria],
);
const [inputs, setInputs] = useState(defaultInputs);

const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
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 (
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
<Typography variant="overline">{t("Search Criteria")}</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
{criteria.map((c) => {
return (
<Grid key={c.paramName} item xs={6}>
{c.type === "text" && (
<TextField
label={c.label}
fullWidth
onChange={makeInputChangeHandler(c.paramName)}
value={inputs[c.paramName]}
/>
)}
{c.type === "select" && (
<FormControl fullWidth>
<InputLabel>{c.label}</InputLabel>
<Select
label={c.label}
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName]}
>
<MenuItem value={"All"}>{t("All")}</MenuItem>
{c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{option}
</MenuItem>
))}
</Select>
</FormControl>
)}
{c.type === "dateRange" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Box display="flex">
<FormControl fullWidth>
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
/>
</FormControl>
<Box
display="flex"
alignItems="center"
justifyContent="center"
marginInline={2}
>
{"-"}
</Box>
<FormControl fullWidth>
<DatePicker
label={c.label2}
onChange={makeDateToChangeHandler(c.paramName)}
value={inputs[c.paramName.concat("To") as T] ? dayjs(inputs[c.paramName.concat("To") as T]) : null}
/>
</FormControl>
</Box>
</LocalizationProvider>
)}
</Grid>
);
})}
</Grid>
<CardActions sx={{ justifyContent: "flex-end" }}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={handleReset}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Search />}
onClick={handleDownload}
>
{t("Download")}
</Button>
</CardActions>
</CardContent>
</Card>
);
}

export default SearchBox;

+ 0
- 3
src/components/Report/ReportSearchBox3/index.ts Dosyayı Görüntüle

@@ -1,3 +0,0 @@
//src\components\SearchBox\index.ts
export { default } from "./SearchBox3";
export type { Criterion } from "./SearchBox3";

+ 0
- 45
src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGen.tsx Dosyayı Görüntüle

@@ -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<Omit<ResourceOverconsumption, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");

const searchCriteria: Criterion<SearchParamNames>[] = 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 (
<>
<SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
{/* <DownloadReportButton /> */}
</>
);
};

export default ProgressByClientSearch;

+ 0
- 19
src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenWrapper.tsx Dosyayı Görüntüle

@@ -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 <ResourceOvercomsumptionReportGen projects={clentprojects} />;
};

ResourceOvercomsumptionReportGenWrapper.Loading = ResourceOvercomsumptionReportGenLoading;

export default ResourceOvercomsumptionReportGenWrapper;

+ 0
- 2
src/components/Report/ResourceOverconsumptionReportGen/index.ts Dosyayı Görüntüle

@@ -1,2 +0,0 @@
//src\components\DelayReportGen\index.ts
export { default } from "./ResourceOverconsumptionReportGenWrapper";

+ 96
- 0
src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReport.tsx Dosyayı Görüntüle

@@ -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<Omit<ProjectResourceOverconsumptionReportFilter, "id">>;
type SearchParamNames = keyof SearchQuery;

const ResourceOverconsumptionReport: React.FC<Props> = ({ 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<SearchParamNames>[] = 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 (
<>
<SearchBox
criteria={searchCriteria}
onSearch={async (query: any) => {
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

src/components/Report/ResourceOverconsumptionReportGen/ResourceOverconsumptionReportGenLoading.tsx → src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportLoading.tsx Dosyayı Görüntüle

@@ -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 (
<>
<Card>
@@ -38,4 +38,4 @@ export const ResourceOvercomsumptionReportGenLoading: React.FC = () => {
);
};

export default ResourceOvercomsumptionReportGenLoading;
export default ResourceOvercomsumptionReportLoading;

+ 20
- 0
src/components/ResourceOverconsumptionReport/ResourceOverconsumptionReportWrapper.tsx Dosyayı Görüntüle

@@ -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 <ResourceOverconsumptionReport team={teams} customer={customers}/>;
};

ResourceOvercomsumptionReportWrapper.Loading = ResourceOvercomsumptionReportLoading;

export default ResourceOvercomsumptionReportWrapper;

+ 1
- 0
src/components/ResourceOverconsumptionReport/index.ts Dosyayı Görüntüle

@@ -0,0 +1 @@
export { default } from "./ResourceOverconsumptionReportWrapper";

+ 51
- 11
src/components/SearchBox/SearchBox.tsx Dosyayı Görüntüle

@@ -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<T extends string> {
label: string;
@@ -49,17 +60,22 @@ interface MonthYearCriterion<T extends string> extends BaseCriterion<T> {
type: "monthYear";
}

interface NumberCriterion<T extends string> extends BaseCriterion<T> {
type: "number";
}

export type Criterion<T extends string> =
| TextCriterion<T>
| SelectCriterion<T>
| DateRangeCriterion<T>
| MonthYearCriterion<T>;
| MonthYearCriterion<T>
| NumberCriterion<T>;

interface Props<T extends string> {
criteria: Criterion<T>[];
onSearch: (inputs: Record<T, string>) => void;
onReset?: () => void;
formType?: String,
formType?: String;
}

function SearchBox<T extends string>({
@@ -75,10 +91,14 @@ function SearchBox<T extends string>({
(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<T, string>
@@ -86,7 +106,7 @@ function SearchBox<T extends string>({
[criteria]
);
const [inputs, setInputs] = useState(defaultInputs);
const makeInputChangeHandler = useCallback(
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => {
return (e) => {
@@ -95,6 +115,15 @@ function SearchBox<T extends string>({
},
[]
);
const makeNumberChangeHandler = useCallback(
(paramName: T): (event: FocusEvent<HTMLInputElement, Element> | PointerEvent<Element> | KeyboardEvent<Element>, 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<T extends string>({

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<T extends string>({
</Select>
</FormControl>
)}
{c.type === "number" && (
<NumberInput
// defaultValue={90}
min={50}
max={99}
onChange={makeNumberChangeHandler(c.paramName)}
endAdornment={<InputAdornment>%</InputAdornment>}
/>
)}
{c.type === "monthYear" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
@@ -242,7 +280,9 @@ function SearchBox<T extends string>({
</Button>
<Button
variant="outlined"
startIcon={(formType === "download" && <FileDownload />) || <Search />}
startIcon={
(formType === "download" && <FileDownload />) || <Search />
}
onClick={handleSearch}
>
{(formType === "download" && t("Download")) || t("Search")}


+ 192
- 0
src/components/utils/numberInput.tsx Dosyayı Görüntüle

@@ -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<HTMLInputElement, Element> | PointerEvent<Element> | KeyboardEvent<Element>
export const NumberInput = React.forwardRef(function CustomNumberInput(
props: NumberInputProps,
ref: React.ForwardedRef<HTMLDivElement>,
) {
return (
<BaseNumberInput
slots={{
root: StyledInputRoot,
input: StyledInputElement,
incrementButton: StyledButton,
decrementButton: StyledButton,
}}
slotProps={{
incrementButton: {
children: '▴',
},
decrementButton: {
children: '▾',
},
}}
{...props}
ref={ref}
/>
);
});
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<number | null>(null);
return (
<NumberInput
aria-label="Demo number input"
placeholder="Type a number…"
value={value}
onChange={(event, val) => 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);
}
`,
);

+ 153
- 0
src/theme/colorConst.js Dosyayı Görüntüle

@@ -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<number | null>(null);
return (
<NumberInput
aria-label="Demo number input"
placeholder="Type a number…"
value={value}
onChange={(event, val) => 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);
}
`,
);

Yükleniyor…
İptal
Kaydet