소스 검색

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 년 전
부모
커밋
d23d6425d9
26개의 변경된 파일387개의 추가작업 그리고 145개의 파일을 삭제
  1. +6
    -1
      src/app/(main)/analytics/ProjectCashFlowReport/page.tsx
  2. +5
    -1
      src/app/(main)/dashboard/ProjectStatusByClient/page.tsx
  3. +11
    -4
      src/app/api/clientprojects/actions.ts
  4. +26
    -10
      src/app/api/reports/actions.ts
  5. +2
    -2
      src/app/api/reports/index.ts
  6. +37
    -25
      src/app/api/team/index.ts
  7. +9
    -0
      src/app/api/user/actions.ts
  8. +15
    -0
      src/app/api/user/index.ts
  9. +18
    -18
      src/components/CreateProject/CreateProject.tsx
  10. +4
    -3
      src/components/CreateProject/Milestone.tsx
  11. +2
    -0
      src/components/CreateProject/MilestoneSection.tsx
  12. +2
    -2
      src/components/CreateProject/StaffAllocation.tsx
  13. +1
    -1
      src/components/CustomDatagrid/CustomDatagrid.tsx
  14. +41
    -10
      src/components/EditUser/EditUser.tsx
  15. +3
    -4
      src/components/EditUser/EditUserWrapper.tsx
  16. +3
    -5
      src/components/EditUser/UserDetail.tsx
  17. +17
    -7
      src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx
  18. +1
    -0
      src/components/GenerateProjectCashFlowReport/GenerateProjectCashFlowReport.tsx
  19. +57
    -20
      src/components/ProgressByClient/ProgressByClient.tsx
  20. +8
    -5
      src/components/ProgressByClientSearch/ProgressByClientSearch.tsx
  21. +32
    -5
      src/components/Report/LateStartReportGen/LateStartReportGen.tsx
  22. +1
    -1
      src/components/Report/LateStartReportGen/LateStartReportGenWrapper.tsx
  23. +28
    -11
      src/components/Report/ReportSearchBox/SearchBox.tsx
  24. +56
    -10
      src/components/SearchBox/SearchBox.tsx
  25. +1
    -0
      src/i18n/en/common.json
  26. +1
    -0
      src/i18n/zh/common.json

+ 6
- 1
src/app/(main)/analytics/ProjectCashFlowReport/page.tsx 파일 보기

@@ -1,18 +1,23 @@
import { Metadata } from "next";
import { Suspense } from "react";
import { I18nProvider } from "@/i18n";
import { I18nProvider, getServerI18n } from "@/i18n";
import { fetchProjects } from "@/app/api/projects";
import GenerateProjectCashFlowReport from "@/components/GenerateProjectCashFlowReport";
import { Typography } from "@mui/material";

export const metadata: Metadata = {
title: "Project Cash Flow Report",
};

const ProjectCashFlowReport: React.FC = async () => {
const { t } = await getServerI18n("reports");
fetchProjects();

return (
<>
<Typography variant="h4" marginInlineEnd={2}>
{t("Project Cash Flow Report")}
</Typography>
<I18nProvider namespaces={["report", "common"]}>
<Suspense fallback={<GenerateProjectCashFlowReport.Loading />}>
<GenerateProjectCashFlowReport />


+ 5
- 1
src/app/(main)/dashboard/ProjectStatusByClient/page.tsx 파일 보기

@@ -1,14 +1,17 @@

import { Metadata } from "next";
import { I18nProvider } from "@/i18n";
import DashboardPage from "@/components/DashboardPage/DashboardPage";
import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton";
import ProgressByClientSearch from "@/components/ProgressByClientSearch";
import { Suspense } from "react";
import { Suspense} from "react";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Typography from "@mui/material/Typography";
import ProgressByClient from "@/components/ProgressByClient";
import { preloadClientProjects } from "@/app/api/clientprojects";
import { ClientProjectResult} from "@/app/api/clientprojects";
import { useSearchParams } from 'next/navigation';

export const metadata: Metadata = {
title: "Project Status by Client",
@@ -24,6 +27,7 @@ const ProjectStatusByClient: React.FC = () => {
<Suspense fallback={<ProgressByClientSearch.Loading />}>
<ProgressByClientSearch />
</Suspense>
<ProgressByClient/>
</I18nProvider>
);
};


+ 11
- 4
src/app/api/clientprojects/actions.ts 파일 보기

@@ -21,8 +21,15 @@ export interface ClientSubsidiaryProjectResult {
comingPaymentMilestone: string;
}

export const fetchAllClientSubsidiaryProjects = cache(async (customerId: number, subsidiaryId: number) => {
return serverFetchJson<ClientSubsidiaryProjectResult[]>(
`${BASE_API_URL}/dashboard/searchCustomerSubsidiaryProject?customerId=${customerId}&subsidiaryId=${subsidiaryId}`
);
export const fetchAllClientSubsidiaryProjects = cache(async (customerId: number, subsidiaryId?: number) => {
if (subsidiaryId === 0){
return serverFetchJson<ClientSubsidiaryProjectResult[]>(
`${BASE_API_URL}/dashboard/searchCustomerSubsidiaryProject?customerId=${customerId}`
);
} else {
return serverFetchJson<ClientSubsidiaryProjectResult[]>(
`${BASE_API_URL}/dashboard/searchCustomerSubsidiaryProject?customerId=${customerId}&subsidiaryId=${subsidiaryId}`
);
}
});

+ 26
- 10
src/app/api/reports/actions.ts 파일 보기

@@ -35,15 +35,31 @@ export const fetchMonthlyWorkHoursReport = async (data: MonthlyWorkHoursReportRe
return reportBlob
};

// export const fetchLateStartReport = async (data: LateStartReportRequest) => {
// const response = await serverFetchBlob<FileResponse>(
// `${BASE_API_URL}/reports/downloadLateStartReport`,
// {
// method: "POST",
// body: JSON.stringify(data),
// headers: { "Content-Type": "application/json" },
// },
// );

// return response;
// };

export const fetchLateStartReport = async (data: LateStartReportRequest) => {
const reportBlob = await serverFetchBlob<FileResponse>(
`${BASE_API_URL}/reports/downloadLateStartReport`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);
const response = await fetch(`${BASE_API_URL}/reports/downloadLateStartReport`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const errorText = await response.text(); // Attempt to read the response text
throw new Error(`Network response was not ok: ${response.status} - ${errorText}`);
}
const blob = await response.blob();
return { fileBlob: blob, fileName: 'Late_Start_Report.xlsx' };
};

return reportBlob
};

+ 2
- 2
src/app/api/reports/index.ts 파일 보기

@@ -31,7 +31,7 @@ export interface LateStartReportFilter {
}

export interface LateStartReportRequest {
team: string[];
client: string[];
team: string;
client: string;
date: any;
}

+ 37
- 25
src/app/api/team/index.ts 파일 보기

@@ -3,34 +3,46 @@ import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import "server-only";


export interface TeamResult {
action: any;
id: number;
teamId: number;
name: string;
code: string;
description: string;
staffId: string;
staffName: string;
posLabel: string;
posCode: string;
teamLead: number;

}
action: any;
id: number;
teamId: number;
name: string;
code: string;
description: string;
staffId: string;
staffName: string;
posLabel: string;
posCode: string;
teamLead: number;

}

export interface comboProp {
id: any;
label: string;
}

export interface combo {
records: comboProp[];
}

export const fetchTeam = cache(async () => {
return serverFetchJson<TeamResult[]>(`${BASE_API_URL}/team`, {
next: { tags: ["team"] },
});
return serverFetchJson<TeamResult[]>(`${BASE_API_URL}/team`, {
next: { tags: ["team"] },
});
});

export const preloadTeamDetail = () => {
fetchTeamDetail();
};
export const preloadTeamDetail = () => {
fetchTeamDetail();
};

export const fetchTeamDetail = cache(async () => {
return serverFetchJson<TeamResult[]>(`${BASE_API_URL}/team/detail`, {
next: { tags: ["team"] },
});
});
return serverFetchJson<TeamResult[]>(`${BASE_API_URL}/team/detail`, {
next: { tags: ["team"] },
});
});

export const fetchTeamCombo = cache(async () => {
return serverFetchJson<combo>(`${BASE_API_URL}/team/combo`);
});

+ 9
- 0
src/app/api/user/actions.ts 파일 보기

@@ -11,6 +11,7 @@ export interface UserInputs {
email?: string;
addAuthIds?: number[];
removeAuthIds?: number[];
password?: string;
}

export interface PasswordInputs {
@@ -50,4 +51,12 @@ export const changePassword = async (data: any) => {
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

export const adminChangePassword = async (data: any) => {
return serverFetchWithNoContent(`${BASE_API_URL}/user/admin-change-password`, {
method: "PATCH",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
};

+ 15
- 0
src/app/api/user/index.ts 파일 보기

@@ -34,6 +34,15 @@ export interface UserDetail {
auths: any[]
}

export type passwordRule = {
min: number;
max: number;
number: boolean;
upperEng: boolean;
lowerEng: boolean;
specialChar: boolean;
}

export const preloadUser = () => {
fetchUser();
};
@@ -52,4 +61,10 @@ export interface UserDetail {
return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user/${id}`, {
next: { tags: ["user"] },
});
});

export const fetchPwRules = cache(async () => {
return serverFetchJson<passwordRule>(`${BASE_API_URL}/user/password-rule`, {
next: { tags: ["pwRule"] },
});
});

+ 18
- 18
src/components/CreateProject/CreateProject.tsx 파일 보기

@@ -170,12 +170,12 @@ const CreateProject: React.FC<Props> = ({

// Tab - Milestone
let projectTotal = 0
const milestonesKeys = Object.keys(data.milestones)
const milestonesKeys = Object.keys(data.milestones).filter(key => taskGroupKeys.includes(key))
milestonesKeys.filter(key => Object.keys(data.taskGroups).includes(key)).forEach(key => {
const { startDate, endDate, payments } = data.milestones[parseFloat(key)]

if (!Boolean(startDate) || startDate === "Invalid Date" || !Boolean(endDate) || endDate === "Invalid Date" || new Date(startDate) > new Date(endDate)) {
formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"})
formProps.setError("milestones", { message: "milestones is not valid", type: "invalid" })
setTabIndex(3)
hasErrors = true
}
@@ -183,8 +183,8 @@ const CreateProject: React.FC<Props> = ({
projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0)
})

if (projectTotal !== data.expectedProjectFee) {
formProps.setError("milestones", {message: "milestones is not valid", type: "invalid"})
if (projectTotal !== data.expectedProjectFee || milestonesKeys.length !== taskGroupKeys.length) {
formProps.setError("milestones", { message: "milestones is not valid", type: "invalid" })
setTabIndex(3)
hasErrors = true
}
@@ -219,7 +219,7 @@ const CreateProject: React.FC<Props> = ({
data.projectActualEnd = dayjs().format("YYYY-MM-DD");
}

data.taskTemplateId = data.taskTemplateId === "All" ? undefined : data.taskTemplateId;
data.taskTemplateId = data.taskTemplateId === "All" ? undefined : data.taskTemplateId;
const response = await saveProject(data);

if (response.id > 0) {
@@ -293,7 +293,7 @@ const CreateProject: React.FC<Props> = ({
{isEditMode && !(formProps.getValues("projectDeleted") === true) && (
<Stack direction="row" gap={1}>
{/* {!formProps.getValues("projectActualStart") && ( */}
{formProps.getValues("projectStatus") === "Pending to Start" && (
{formProps.getValues("projectStatus").toLowerCase() === "pending to start" && (
<Button
name="start"
type="submit"
@@ -306,21 +306,21 @@ const CreateProject: React.FC<Props> = ({
)}
{/* {formProps.getValues("projectActualStart") &&
!formProps.getValues("projectActualEnd") && ( */}
{formProps.getValues("projectStatus") === "On-going" && (
<Button
name="complete"
type="submit"
variant="contained"
startIcon={<DoneIcon />}
color="info"
>
{t("Complete Project")}
</Button>
)}
{formProps.getValues("projectStatus").toLowerCase() === "on-going" && (
<Button
name="complete"
type="submit"
variant="contained"
startIcon={<DoneIcon />}
color="info"
>
{t("Complete Project")}
</Button>
)}
{!(
// formProps.getValues("projectActualStart") &&
// formProps.getValues("projectActualEnd")
formProps.getValues("projectStatus") === "Completed" ||
formProps.getValues("projectStatus") === "Completed" ||
formProps.getValues("projectStatus") === "Deleted"
) && (
<Button


+ 4
- 3
src/components/CreateProject/Milestone.tsx 파일 보기

@@ -61,11 +61,12 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => {
const milestones = watch("milestones")
const expectedTotalFee = watch("expectedProjectFee");
useEffect(() => {
const milestonesKeys = Object.keys(milestones)
const taskGroupsIds = taskGroups.map(taskGroup => taskGroup.id.toString())
const milestonesKeys = Object.keys(milestones).filter(key => taskGroupsIds.includes(key))
let hasError = false
let projectTotal = 0

milestonesKeys.filter(key => taskGroups.map(taskGroup => taskGroup.id).includes(parseInt(key))).forEach(key => {
milestonesKeys.forEach(key => {
const { startDate, endDate, payments } = milestones[parseFloat(key)]

if (new Date(startDate) > new Date(endDate) || !Boolean(startDate) || !Boolean(endDate)) {
@@ -75,7 +76,7 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => {
projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0)
})

if (projectTotal !== expectedTotalFee) {
if (projectTotal !== expectedTotalFee || milestonesKeys.length !== taskGroupsIds.length) {
hasError = true
}
// console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0))


+ 2
- 0
src/components/CreateProject/MilestoneSection.tsx 파일 보기

@@ -244,6 +244,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
<DatePicker
label={t("Stage Start Date")}
value={startDate ? dayjs(startDate) : null}
format="YYYY/MM/DD"
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");
@@ -272,6 +273,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
<DatePicker
label={t("Stage End Date")}
value={endDate ? dayjs(endDate) : null}
format="YYYY/MM/DD"
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");


+ 2
- 2
src/components/CreateProject/StaffAllocation.tsx 파일 보기

@@ -39,8 +39,8 @@ import { StaffResult } from "@/app/api/staff";

const staffComparator = (a: StaffResult, b: StaffResult) => {
return (
a.team.localeCompare(b.team) ||
a.grade.localeCompare(b.grade) ||
a.team?.localeCompare(b.team) ||
a.grade?.localeCompare(b.grade) ||
a.id - b.id
);
};


+ 1
- 1
src/components/CustomDatagrid/CustomDatagrid.tsx 파일 보기

@@ -1,7 +1,7 @@
"use client";
import * as React from "react";
import { Card, CardHeader, CardContent, SxProps, Theme } from "@mui/material";
import { DataGrid, GridColDef, GridRowSelectionModel, GridColumnGroupingModel} from "@mui/x-data-grid";
import { DataGrid, GridColDef, GridRowSelectionModel, GridColumnGroupingModel, useGridApiRef} from "@mui/x-data-grid";
import { darken, lighten, styled } from "@mui/material/styles";
import { useState } from "react";



+ 41
- 10
src/components/EditUser/EditUser.tsx 파일 보기

@@ -26,17 +26,19 @@ import {
} from "react-hook-form";
import { Check, Close, Error, RestartAlt } from "@mui/icons-material";
import { StaffResult } from "@/app/api/staff";
import { UserInputs, editUser, fetchUserDetails } from "@/app/api/user/actions";
import { UserInputs, adminChangePassword, editUser, fetchUserDetails } from "@/app/api/user/actions";
import UserDetail from "./UserDetail";
import { UserResult } from "@/app/api/user";
import { UserResult, passwordRule } from "@/app/api/user";
import { auth, fetchAuth } from "@/app/api/group/actions";
import AuthAllocation from "./AuthAllocation";

interface Props {
// users: UserResult[]
}
rules: passwordRule
}

const EditUser: React.FC<Props> = async ({ }) => {
const EditUser: React.FC<Props> = async ({
rules
}) => {
const { t } = useTranslation();
const formProps = useForm<UserInputs>();
const searchParams = useSearchParams();
@@ -53,11 +55,11 @@ const EditUser: React.FC<Props> = async ({ }) => {
},
[]
);
console.log(rules);

const errors = formProps.formState.errors;

const fetchUserDetail = async () => {
console.log(id);
try {
// fetch user info
const userDetail = await fetchUserDetails(id);
@@ -112,16 +114,45 @@ const EditUser: React.FC<Props> = async ({ }) => {
const onSubmit = useCallback<SubmitHandler<UserInputs>>(
async (data) => {
try {
let haveError = false
let regex_pw = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
let pw = ''
if (data.password && data.password.length > 0) {
pw = data.password
if (pw.length < rules.min) {
haveError = true
formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" })
}
if (pw.length > rules.max) {
haveError = true
formProps.setError("password", { message: t("The password requires 8-20 characters."), type: "required" })
}
if (!regex_pw.test(pw)) {
haveError = true
formProps.setError("password", { message: "A combination of uppercase letters, lowercase letters, numbers, and symbols is required.", type: "required" })
}
}
console.log(data);
const tempData = {
const userData = {
name: data.name,
email: data.email,
email: '',
locked: false,
addAuthIds: data.addAuthIds || [],
removeAuthIds: data.removeAuthIds || [],
}
console.log(tempData);
await editUser(id, tempData);
const pwData = {
id: id,
password: "",
newPassword: pw
}
console.log(userData);
if (haveError) {
return
}
await editUser(id, userData);
if (data.password && data.password.length > 0) {
await adminChangePassword(pwData);
}
router.replace("/settings/staff");
} catch (e) {
console.log(e);


+ 3
- 4
src/components/EditUser/EditUserWrapper.tsx 파일 보기

@@ -5,7 +5,7 @@ import EditUserLoading from "./EditUserLoading";
import { useSearchParams } from "next/navigation";
import { fetchTeam, fetchTeamDetail } from "@/app/api/team";
import { fetchStaff } from "@/app/api/staff";
import { fetchUser, fetchUserDetail } from "@/app/api/user";
import { fetchPwRules, fetchUser, fetchUserDetail } from "@/app/api/user";

interface SubComponents {
Loading: typeof EditUserLoading;
@@ -17,10 +17,9 @@ interface Props {
const EditUserWrapper: React.FC<Props> & SubComponents = async ({
// id
}) => {
// const users = await fetchUser()
// const userDetail = await fetchUserDetail(id)
const pwRule = await fetchPwRules()

return <EditUser />
return <EditUser rules={pwRule} />
};

EditUserWrapper.Loading = EditUserLoading;


+ 3
- 5
src/components/EditUser/UserDetail.tsx 파일 보기

@@ -47,12 +47,10 @@ const UserDetail: React.FC<Props> = ({
</Grid>
<Grid item xs={6}>
<TextField
label={t("email")}
label={t("password")}
fullWidth
{...register("email", {
required: "email required!",
})}
error={Boolean(errors.email)}
{...register("password")}
error={Boolean(errors.password)}
/>
</Grid>
</Grid>


+ 17
- 7
src/components/GenerateMonthlyWorkHoursReport/GenerateMonthlyWorkHoursReport.tsx 파일 보기

@@ -24,11 +24,18 @@ const GenerateMonthlyWorkHoursReport: React.FC<Props> = ({ staffs }) => {

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: t("Staff"),
paramName: "staff",
type: "select",
options: staffCombo,
needAll: false},
{
label: t("Staff"),
paramName: "staff",
type: "select",
options: staffCombo,
needAll: false
},
{
label: t("date"),
paramName: "date",
type: "monthYear",
},
],
[t],
);
@@ -38,9 +45,12 @@ return (
<SearchBox
criteria={searchCriteria}
onSearch={async (query: any) => {
if (query.staff.length > 0 && query.staff.toLocaleLowerCase() !== "all") {
console.log(query)
console.log(query.date.length)
if (query.staff.length > 0 && query.staff.toLocaleLowerCase() !== "all" && query.date.length > 0) {
const index = staffCombo.findIndex(staff => staff === query.staff)
const response = await fetchMonthlyWorkHoursReport({ id: staffs[index].id, yearMonth: "2023-03" })
console.log(index)
const response = await fetchMonthlyWorkHoursReport({ id: staffs[index].id, yearMonth: query.date })
if (response) {
downloadFile(new Uint8Array(response.blobValue), response.filename!!)
}


+ 1
- 0
src/components/GenerateProjectCashFlowReport/GenerateProjectCashFlowReport.tsx 파일 보기

@@ -41,6 +41,7 @@ const GenerateProjectCashFlowReport: React.FC<Props> = ({ projects }) => {
}
}
}}
formType={"download"}
/>
</>
);


+ 57
- 20
src/components/ProgressByClient/ProgressByClient.tsx 파일 보기

@@ -21,13 +21,18 @@ import { ClientProjectResult} from "@/app/api/clientprojects";
import { ConstructionOutlined } from "@mui/icons-material";
import ReactApexChart from "react-apexcharts";
import { ApexOptions } from "apexcharts";
import { useSearchParams } from 'next/navigation';
import { fetchAllClientSubsidiaryProjects} from "@/app/api/clientprojects/actions";
// const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false });

interface Props {
clientSubsidiaryProjectResult: ClientSubsidiaryProjectResult[];
// clientSubsidiaryProjectResult: ClientSubsidiaryProjectResult[];
}

const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => {
const ProgressByClient: React.FC<Props> = () => {
const searchParams = useSearchParams();
const customerId = searchParams.get('customerId');
const subsidiaryId = searchParams.get('subsidiaryId');
const [activeTab, setActiveTab] = useState("financialSummary");
const [SearchCriteria, setSearchCriteria] = React.useState({});
const { t } = useTranslation("dashboard");
@@ -56,6 +61,32 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
const [chartProjectName, setChartProjectName]:any[] = useState([]);
const [chartManhourConsumptionPercentage, setChartManhourConsumptionPercentage]:any[] = useState([]);
const color = ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b"];
const [clientSubsidiaryProjectResult, setClientSubsidiaryProjectResult]:any[] = useState([]);

const fetchData = async () => {
if (customerId && subsidiaryId) {
try {
if (subsidiaryId === '-'){
console.log("ss")
const clickResult = await fetchAllClientSubsidiaryProjects(
Number(customerId),Number(0))
console.log(clickResult)
setClientSubsidiaryProjectResult(clickResult);
} else {
const clickResult = await fetchAllClientSubsidiaryProjects(
Number(customerId),
Number(subsidiaryId))
console.log(clickResult)
setClientSubsidiaryProjectResult(clickResult);
}
} catch (error) {
console.error('Error fetching client subsidiary projects:', error);
}
}
}
useEffect(() => {
const projectName = []
const manhourConsumptionPercentage = []
@@ -68,6 +99,12 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
setChartManhourConsumptionPercentage(manhourConsumptionPercentage)
}, [clientSubsidiaryProjectResult]);

useEffect(() => {
fetchData()
}, [customerId,subsidiaryId]);



const rows2 = [
{
id: 1,
@@ -155,37 +192,37 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
></span>
);
},
flex: 0.1,
flex:0.1
},
{
id: "projectName",
field: "projectName",
headerName: "Project",
flex: 1,
minWidth:300
},
{
id: "team",
field: "team",
headerName: "Team",
flex: 0.8,
minWidth: 50
},
{
id: "teamLeader",
field: "teamLeader",
id: "teamLead",
field: "teamLead",
headerName: "Team Leader",
flex: 0.8,
minWidth: 70
},
{
id: "expectedStage",
field: "expectedStage",
headerName: "Expected Stage",
flex: 1,
minWidth: 300
},
{
id: "budgetedManhour",
field: "budgetedManhour",
headerName: "Budgeted Manhour",
flex: 0.8,
minWidth: 70
},
{
id: "spentManhour",
@@ -201,7 +238,7 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
return <span>{params.row.spentManhour}</span>;
}
},
flex: 0.8,
minWidth: 70
},
{
id: "remainedManhour",
@@ -216,13 +253,13 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
return <span>{params.row.remainedManhour}</span>;
}
},
flex: 1,
minWidth: 70
},
{
id: "comingPaymentMilestone",
field: "comingPaymentMilestone",
headerName: "Coming Payment Milestone",
flex: 1,
minWidth: 100
},
{
id: "alert",
@@ -239,7 +276,7 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
return <span></span>;
}
},
flex: 0.2,
flex:0.1
},
];
const optionstest: ApexOptions = {
@@ -463,7 +500,7 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
};

const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => {
const selectedRowsData:any = clientSubsidiaryProjectResult.filter((row) =>
const selectedRowsData:any = clientSubsidiaryProjectResult.filter((row:any) =>
newSelectionModel.includes(row.projectId),
);
console.log(selectedRowsData);
@@ -533,9 +570,9 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
<CardHeader className="text-slate-500" title="Project Resource Consumption" />
<div style={{ display: "inline-block", width: "99%" }}>
<ReactApexChart
options={optionstest}
series={optionstest.series}
type="line"
options={options}
series={options.series}
type="bar"
height={350}
/>
</div>
@@ -590,13 +627,13 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) =>
Please select the project you want to check.
</div>
)}
{/* {percentageArray.length > 0 && (
{percentageArray.length > 0 && (
<ReactApexChart
options={options2}
series={percentageArray}
type="donut"
/>
)} */}
)}
</Card>
</Grid>
<Grid item xs={12} md={12} lg={12}>


+ 8
- 5
src/components/ProgressByClientSearch/ProgressByClientSearch.tsx 파일 보기

@@ -1,7 +1,7 @@
"use client";

import { ProjectResult } from "@/app/api/projects";
import React, { useMemo, useState, useCallback } from "react";
import React, { useMemo, useState, useCallback, useEffect } from "react";
import SearchBox, { Criterion } from "../SearchBox";
import { useTranslation } from "react-i18next";
import SearchResults, { Column } from "../SearchResults";
@@ -18,6 +18,7 @@ type SearchQuery = Partial<Omit<ClientProjectResult, "id">>;
type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ clientProjects }) => {
const router = useRouter();
const { t } = useTranslation("projects");
const searchParams = useSearchParams()
// If project searching is done on the server-side, then no need for this.
@@ -41,9 +42,9 @@ const ProgressByClientSearch: React.FC<Props> = ({ clientProjects }) => {

const onTaskClick = useCallback(async (clientProjectResult: ClientProjectResult) => {
try {
const clickResult = await fetchAllClientSubsidiaryProjects(clientProjectResult.customerId, clientProjectResult.subsidiaryId);
console.log(clickResult);
setClientSubsidiaryProjectResult(clickResult);
router.push(
`/dashboard/ProjectStatusByClient?customerId=${clientProjectResult.customerId}&subsidiaryId=${clientProjectResult.subsidiaryId}`
);
} catch (error) {
console.error('Error fetching client subsidiary projects:', error);
}
@@ -86,7 +87,9 @@ const ProgressByClientSearch: React.FC<Props> = ({ clientProjects }) => {
items={filteredProjects}
columns={columns}
/>
<ProgressByClient clientSubsidiaryProjectResult={clientSubsidiaryProjectResult}/>
{/* {clientSubsidiaryProjectResult.length > 0 && (
<ProgressByClient clientSubsidiaryProjectResult={clientSubsidiaryProjectResult} />
)} */}
</>
);
};


+ 32
- 5
src/components/Report/LateStartReportGen/LateStartReportGen.tsx 파일 보기

@@ -1,12 +1,13 @@
//src\components\LateStartReportGen\LateStartReportGen.tsx
"use client";
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import SearchBox, { Criterion } from "../ReportSearchBox";
import { useTranslation } from "react-i18next";
import { LateStart } from "@/app/api/report";
//import { DownloadReportButton } from './DownloadReportButton';
// import axios from 'axios';
import { apiPath } from '../../../auth/utils';
import { fetchTeamCombo } from "@/app/api/team/actions";
//import { GET_QC_CATEGORY_COMBO } from 'utils/ApiPathConst';
interface Props {
projects: LateStart[];
@@ -16,6 +17,9 @@ type SearchParamNames = keyof SearchQuery;

const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
const { t } = useTranslation("projects");
const [teamCombo, setteamCombo] = useState<string[]>([])
const [clientCombo, setclientCombo] = useState<string[]>([])
const [isLoading, setIsLoading] = useState(true)
// const [teamCombo, setteamCombo] = useState([]);
// const getteamCombo = () => {
// axios.get(`${apiPath}${GET_QC_CATEGORY_COMBO}`)
@@ -25,9 +29,32 @@ const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
// console.error('Error fetching data: ', error);
// });
// }

const getTeamCombo = async() => {
try {
const response = await fetchTeamCombo()
setteamCombo(response.records.map(record => record.label))
setIsLoading(false)
} catch (err) {
console.log(err)
}
}
// const getClientCombo = async() => {
// try {
// const response = await fetchCombo()
// setclientCombo(response.records.map(record => record.label))
// setIsLoading(false)
// } catch (err) {
// console.log(err)
// }
// }
useEffect(() => {
getTeamCombo()
}, [])

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{ label: "Team", paramName: "team", type: "select", options: ["AAA", "BBB", "CCC"] },
{ label: "Team", paramName: "team", type: "select", options: teamCombo },
{ label: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] },
{
label: "Remained Date From",
@@ -36,17 +63,17 @@ const ProgressByClientSearch: React.FC<Props> = ({ projects }) => {
type: "dateRange",
},
],
[t],
[t, teamCombo],
);

return (
<>
<SearchBox
{!isLoading && <SearchBox
criteria={searchCriteria}
onSearch={(query) => {
console.log(query);
}}
/>
/>}
{/* <DownloadReportButton /> */}
</>
);


+ 1
- 1
src/components/Report/LateStartReportGen/LateStartReportGenWrapper.tsx 파일 보기

@@ -11,7 +11,7 @@ interface SubComponents {
const LateStartReportGenWrapper: React.FC & SubComponents = async () => {
const clentprojects = await fetchProjectsLateStart();

return <LateStartReportGen projects={clentprojects} />;
return <LateStartReportGen projects={clentprojects}/>;
};

LateStartReportGenWrapper.Loading = LateStartReportGenLoading;


+ 28
- 11
src/components/Report/ReportSearchBox/SearchBox.tsx 파일 보기

@@ -22,7 +22,10 @@ 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 { fetchLateStartReport } from "@/app/api/reports/actions";
import * as XLSX from 'xlsx-js-style';
import { LateStartReportRequest } from "@/app/api/reports";
import { fetchTeamCombo } from "@/app/api/team/actions";
//import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton';

interface BaseCriterion<T extends string> {
@@ -113,25 +116,39 @@ function SearchBox<T extends string>({
onSearch(inputs);
};

//fetchLateStartReport
const handleDownload = async () => {
try {
const response = await fetch('/api/reports', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ projectId: '123' }), // Example payload
});
if (!response.ok) throw new Error('Network response was not ok.');
// Create a request object, which includes the projectId
const abc = await fetchTeamCombo()

//console.log(abc.records)
const requestData: LateStartReportRequest = {
team: 'Your Team Name', // Example value, adjust as necessary
client: 'Client Name', // Example value, adjust as necessary
date: new Date().toISOString() // Current date in ISO format, adjust as necessary
};
// Call fetchLateStartReport and wait for the blob
//const responseBlob = await fetchLateStartReport(requestData);
const fileResponse = await fetchLateStartReport(requestData);
const blob = fileResponse.fileBlob;
const data = await response.blob();
const url = window.URL.createObjectURL(data);
// Create a URL from the Blob response
const url = window.URL.createObjectURL(blob);
// Create an anchor element and trigger the download
const a = document.createElement('a');
a.href = url;
a.download = "Project_Cash_Flow_Report.xlsx";
a.download = "Late_Start_Report.xlsx"; // Set the filename for download
document.body.appendChild(a);
a.click();
a.remove();
// Optionally revoke the URL if you want to free up resources
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error downloading the file: ', error);
}


+ 56
- 10
src/components/SearchBox/SearchBox.tsx 파일 보기

@@ -15,12 +15,14 @@ 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 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 { DateCalendar } from "@mui/x-date-pickers";

interface BaseCriterion<T extends string> {
label: string;
@@ -43,21 +45,28 @@ interface DateRangeCriterion<T extends string> extends BaseCriterion<T> {
type: "dateRange";
}

interface MonthYearCriterion<T extends string> extends BaseCriterion<T> {
type: "monthYear";
}

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

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

function SearchBox<T extends string>({
criteria,
onSearch,
onReset,
formType,
}: Props<T>) {
const { t } = useTranslation("common");
const defaultInputs = useMemo(
@@ -66,19 +75,19 @@ function SearchBox<T extends string>({
(acc, c) => {
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" };
},
{} as Record<T, string>,
{} as Record<T, string>
),
[criteria],
[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) => {
@@ -93,6 +102,13 @@ function SearchBox<T extends string>({
};
}, []);

const makeMonthYearChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
console.log(dayjs(e).format("YYYY-MM"))
setInputs((i) => ({ ...i, [paramName]: dayjs(e).format("YYYY-MM") }));
};
}, []);

const makeDateToChangeHandler = useCallback((paramName: T) => {
return (e: any) => {
setInputs((i) => ({
@@ -135,7 +151,9 @@ function SearchBox<T extends string>({
onChange={makeSelectChangeHandler(c.paramName)}
value={inputs[c.paramName]}
>
{!(c.needAll === false) && <MenuItem value={"All"}>{t("All")}</MenuItem>}
{!(c.needAll === false) && (
<MenuItem value={"All"}>{t("All")}</MenuItem>
)}
{c.options.map((option, index) => (
<MenuItem key={`${option}-${index}`} value={option}>
{t(option)}
@@ -144,6 +162,26 @@ function SearchBox<T extends string>({
</Select>
</FormControl>
)}
{c.type === "monthYear" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
// TODO: Should maybe use a custom adapterLocale here to support YYYY-MM-DD
adapterLocale="zh-hk"
>
<Box display="flex">
<DateCalendar
views={["month", "year"]}
openTo="month"
onChange={makeMonthYearChangeHandler(c.paramName)}
value={
inputs[c.paramName]
? dayjs(inputs[c.paramName])
: null
}
/>
</Box>
</LocalizationProvider>
)}
{c.type === "dateRange" && (
<LocalizationProvider
dateAdapter={AdapterDayjs}
@@ -155,7 +193,11 @@ function SearchBox<T extends string>({
<DatePicker
label={c.label}
onChange={makeDateChangeHandler(c.paramName)}
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null}
value={
inputs[c.paramName]
? dayjs(inputs[c.paramName])
: null
}
/>
</FormControl>
<Box
@@ -170,7 +212,11 @@ function SearchBox<T extends string>({
<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}
value={
inputs[c.paramName.concat("To") as T]
? dayjs(inputs[c.paramName.concat("To") as T])
: null
}
/>
</FormControl>
</Box>
@@ -190,10 +236,10 @@ function SearchBox<T extends string>({
</Button>
<Button
variant="outlined"
startIcon={<Search />}
startIcon={(formType === "download" && <FileDownload />) || <Search />}
onClick={handleSearch}
>
{t("Search")}
{(formType === "download" && t("Download")) || t("Search")}
</Button>
</CardActions>
</CardContent>


+ 1
- 0
src/i18n/en/common.json 파일 보기

@@ -19,6 +19,7 @@
"Details": "Details",
"Delete": "Delete",
"Download": "Download",
"Search": "Search",
"Search Criteria": "Search Criteria",
"Cancel": "Cancel",


+ 1
- 0
src/i18n/zh/common.json 파일 보기

@@ -17,6 +17,7 @@
"Details": "詳情",
"Delete": "刪除",
"Download": "下載",
"Search": "搜尋",
"Search Criteria": "搜尋條件",
"Cancel": "取消",


불러오는 중...
취소
저장