@@ -1,18 +1,23 @@ | |||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { Suspense } from "react"; | import { Suspense } from "react"; | ||||
import { I18nProvider } from "@/i18n"; | |||||
import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
import { fetchProjects } from "@/app/api/projects"; | import { fetchProjects } from "@/app/api/projects"; | ||||
import GenerateProjectCashFlowReport from "@/components/GenerateProjectCashFlowReport"; | import GenerateProjectCashFlowReport from "@/components/GenerateProjectCashFlowReport"; | ||||
import { Typography } from "@mui/material"; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Project Cash Flow Report", | title: "Project Cash Flow Report", | ||||
}; | }; | ||||
const ProjectCashFlowReport: React.FC = async () => { | const ProjectCashFlowReport: React.FC = async () => { | ||||
const { t } = await getServerI18n("reports"); | |||||
fetchProjects(); | fetchProjects(); | ||||
return ( | return ( | ||||
<> | <> | ||||
<Typography variant="h4" marginInlineEnd={2}> | |||||
{t("Project Cash Flow Report")} | |||||
</Typography> | |||||
<I18nProvider namespaces={["report", "common"]}> | <I18nProvider namespaces={["report", "common"]}> | ||||
<Suspense fallback={<GenerateProjectCashFlowReport.Loading />}> | <Suspense fallback={<GenerateProjectCashFlowReport.Loading />}> | ||||
<GenerateProjectCashFlowReport /> | <GenerateProjectCashFlowReport /> | ||||
@@ -1,14 +1,17 @@ | |||||
import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
import { I18nProvider } from "@/i18n"; | import { I18nProvider } from "@/i18n"; | ||||
import DashboardPage from "@/components/DashboardPage/DashboardPage"; | import DashboardPage from "@/components/DashboardPage/DashboardPage"; | ||||
import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton"; | import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton"; | ||||
import ProgressByClientSearch from "@/components/ProgressByClientSearch"; | import ProgressByClientSearch from "@/components/ProgressByClientSearch"; | ||||
import { Suspense } from "react"; | |||||
import { Suspense} from "react"; | |||||
import Tabs, { TabsProps } from "@mui/material/Tabs"; | import Tabs, { TabsProps } from "@mui/material/Tabs"; | ||||
import Tab from "@mui/material/Tab"; | import Tab from "@mui/material/Tab"; | ||||
import Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
import ProgressByClient from "@/components/ProgressByClient"; | import ProgressByClient from "@/components/ProgressByClient"; | ||||
import { preloadClientProjects } from "@/app/api/clientprojects"; | import { preloadClientProjects } from "@/app/api/clientprojects"; | ||||
import { ClientProjectResult} from "@/app/api/clientprojects"; | |||||
import { useSearchParams } from 'next/navigation'; | |||||
export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
title: "Project Status by Client", | title: "Project Status by Client", | ||||
@@ -24,6 +27,7 @@ const ProjectStatusByClient: React.FC = () => { | |||||
<Suspense fallback={<ProgressByClientSearch.Loading />}> | <Suspense fallback={<ProgressByClientSearch.Loading />}> | ||||
<ProgressByClientSearch /> | <ProgressByClientSearch /> | ||||
</Suspense> | </Suspense> | ||||
<ProgressByClient/> | |||||
</I18nProvider> | </I18nProvider> | ||||
); | ); | ||||
}; | }; | ||||
@@ -21,8 +21,15 @@ export interface ClientSubsidiaryProjectResult { | |||||
comingPaymentMilestone: string; | 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}` | |||||
); | |||||
} | |||||
}); | }); |
@@ -35,15 +35,31 @@ export const fetchMonthlyWorkHoursReport = async (data: MonthlyWorkHoursReportRe | |||||
return reportBlob | 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) => { | 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 | |||||
}; |
@@ -31,7 +31,7 @@ export interface LateStartReportFilter { | |||||
} | } | ||||
export interface LateStartReportRequest { | export interface LateStartReportRequest { | ||||
team: string[]; | |||||
client: string[]; | |||||
team: string; | |||||
client: string; | |||||
date: any; | date: any; | ||||
} | } |
@@ -3,34 +3,46 @@ import { BASE_API_URL } from "@/config/api"; | |||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
export interface TeamResult { | 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 () => { | 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 () => { | 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`); | |||||
}); |
@@ -11,6 +11,7 @@ export interface UserInputs { | |||||
email?: string; | email?: string; | ||||
addAuthIds?: number[]; | addAuthIds?: number[]; | ||||
removeAuthIds?: number[]; | removeAuthIds?: number[]; | ||||
password?: string; | |||||
} | } | ||||
export interface PasswordInputs { | export interface PasswordInputs { | ||||
@@ -50,4 +51,12 @@ export const changePassword = async (data: any) => { | |||||
body: JSON.stringify(data), | body: JSON.stringify(data), | ||||
headers: { "Content-Type": "application/json" }, | 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" }, | |||||
}); | |||||
}; | }; |
@@ -34,6 +34,15 @@ export interface UserDetail { | |||||
auths: any[] | auths: any[] | ||||
} | } | ||||
export type passwordRule = { | |||||
min: number; | |||||
max: number; | |||||
number: boolean; | |||||
upperEng: boolean; | |||||
lowerEng: boolean; | |||||
specialChar: boolean; | |||||
} | |||||
export const preloadUser = () => { | export const preloadUser = () => { | ||||
fetchUser(); | fetchUser(); | ||||
}; | }; | ||||
@@ -52,4 +61,10 @@ export interface UserDetail { | |||||
return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user/${id}`, { | return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user/${id}`, { | ||||
next: { tags: ["user"] }, | next: { tags: ["user"] }, | ||||
}); | }); | ||||
}); | |||||
export const fetchPwRules = cache(async () => { | |||||
return serverFetchJson<passwordRule>(`${BASE_API_URL}/user/password-rule`, { | |||||
next: { tags: ["pwRule"] }, | |||||
}); | |||||
}); | }); |
@@ -170,12 +170,12 @@ const CreateProject: React.FC<Props> = ({ | |||||
// Tab - Milestone | // Tab - Milestone | ||||
let projectTotal = 0 | 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 => { | milestonesKeys.filter(key => Object.keys(data.taskGroups).includes(key)).forEach(key => { | ||||
const { startDate, endDate, payments } = data.milestones[parseFloat(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)) { | 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) | setTabIndex(3) | ||||
hasErrors = true | hasErrors = true | ||||
} | } | ||||
@@ -183,8 +183,8 @@ const CreateProject: React.FC<Props> = ({ | |||||
projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0) | 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) | setTabIndex(3) | ||||
hasErrors = true | hasErrors = true | ||||
} | } | ||||
@@ -219,7 +219,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
data.projectActualEnd = dayjs().format("YYYY-MM-DD"); | 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); | const response = await saveProject(data); | ||||
if (response.id > 0) { | if (response.id > 0) { | ||||
@@ -293,7 +293,7 @@ const CreateProject: React.FC<Props> = ({ | |||||
{isEditMode && !(formProps.getValues("projectDeleted") === true) && ( | {isEditMode && !(formProps.getValues("projectDeleted") === true) && ( | ||||
<Stack direction="row" gap={1}> | <Stack direction="row" gap={1}> | ||||
{/* {!formProps.getValues("projectActualStart") && ( */} | {/* {!formProps.getValues("projectActualStart") && ( */} | ||||
{formProps.getValues("projectStatus") === "Pending to Start" && ( | |||||
{formProps.getValues("projectStatus").toLowerCase() === "pending to start" && ( | |||||
<Button | <Button | ||||
name="start" | name="start" | ||||
type="submit" | type="submit" | ||||
@@ -306,21 +306,21 @@ const CreateProject: React.FC<Props> = ({ | |||||
)} | )} | ||||
{/* {formProps.getValues("projectActualStart") && | {/* {formProps.getValues("projectActualStart") && | ||||
!formProps.getValues("projectActualEnd") && ( */} | !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("projectActualStart") && | ||||
// formProps.getValues("projectActualEnd") | // formProps.getValues("projectActualEnd") | ||||
formProps.getValues("projectStatus") === "Completed" || | |||||
formProps.getValues("projectStatus") === "Completed" || | |||||
formProps.getValues("projectStatus") === "Deleted" | formProps.getValues("projectStatus") === "Deleted" | ||||
) && ( | ) && ( | ||||
<Button | <Button | ||||
@@ -61,11 +61,12 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||||
const milestones = watch("milestones") | const milestones = watch("milestones") | ||||
const expectedTotalFee = watch("expectedProjectFee"); | const expectedTotalFee = watch("expectedProjectFee"); | ||||
useEffect(() => { | 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 hasError = false | ||||
let projectTotal = 0 | 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)] | const { startDate, endDate, payments } = milestones[parseFloat(key)] | ||||
if (new Date(startDate) > new Date(endDate) || !Boolean(startDate) || !Boolean(endDate)) { | 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) | projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0) | ||||
}) | }) | ||||
if (projectTotal !== expectedTotalFee) { | |||||
if (projectTotal !== expectedTotalFee || milestonesKeys.length !== taskGroupsIds.length) { | |||||
hasError = true | hasError = true | ||||
} | } | ||||
// console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) | // console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) | ||||
@@ -244,6 +244,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
<DatePicker | <DatePicker | ||||
label={t("Stage Start Date")} | label={t("Stage Start Date")} | ||||
value={startDate ? dayjs(startDate) : null} | value={startDate ? dayjs(startDate) : null} | ||||
format="YYYY/MM/DD" | |||||
onChange={(date) => { | onChange={(date) => { | ||||
if (!date) return; | if (!date) return; | ||||
const milestones = getValues("milestones"); | const milestones = getValues("milestones"); | ||||
@@ -272,6 +273,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
<DatePicker | <DatePicker | ||||
label={t("Stage End Date")} | label={t("Stage End Date")} | ||||
value={endDate ? dayjs(endDate) : null} | value={endDate ? dayjs(endDate) : null} | ||||
format="YYYY/MM/DD" | |||||
onChange={(date) => { | onChange={(date) => { | ||||
if (!date) return; | if (!date) return; | ||||
const milestones = getValues("milestones"); | const milestones = getValues("milestones"); | ||||
@@ -39,8 +39,8 @@ import { StaffResult } from "@/app/api/staff"; | |||||
const staffComparator = (a: StaffResult, b: StaffResult) => { | const staffComparator = (a: StaffResult, b: StaffResult) => { | ||||
return ( | 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 | a.id - b.id | ||||
); | ); | ||||
}; | }; | ||||
@@ -1,7 +1,7 @@ | |||||
"use client"; | "use client"; | ||||
import * as React from "react"; | import * as React from "react"; | ||||
import { Card, CardHeader, CardContent, SxProps, Theme } from "@mui/material"; | 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 { darken, lighten, styled } from "@mui/material/styles"; | ||||
import { useState } from "react"; | import { useState } from "react"; | ||||
@@ -26,17 +26,19 @@ import { | |||||
} from "react-hook-form"; | } from "react-hook-form"; | ||||
import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; | import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; | ||||
import { StaffResult } from "@/app/api/staff"; | 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 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 { auth, fetchAuth } from "@/app/api/group/actions"; | ||||
import AuthAllocation from "./AuthAllocation"; | import AuthAllocation from "./AuthAllocation"; | ||||
interface Props { | interface Props { | ||||
// users: UserResult[] | |||||
} | |||||
rules: passwordRule | |||||
} | |||||
const EditUser: React.FC<Props> = async ({ }) => { | |||||
const EditUser: React.FC<Props> = async ({ | |||||
rules | |||||
}) => { | |||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
const formProps = useForm<UserInputs>(); | const formProps = useForm<UserInputs>(); | ||||
const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
@@ -53,11 +55,11 @@ const EditUser: React.FC<Props> = async ({ }) => { | |||||
}, | }, | ||||
[] | [] | ||||
); | ); | ||||
console.log(rules); | |||||
const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
const fetchUserDetail = async () => { | const fetchUserDetail = async () => { | ||||
console.log(id); | |||||
try { | try { | ||||
// fetch user info | // fetch user info | ||||
const userDetail = await fetchUserDetails(id); | const userDetail = await fetchUserDetails(id); | ||||
@@ -112,16 +114,45 @@ const EditUser: React.FC<Props> = async ({ }) => { | |||||
const onSubmit = useCallback<SubmitHandler<UserInputs>>( | const onSubmit = useCallback<SubmitHandler<UserInputs>>( | ||||
async (data) => { | async (data) => { | ||||
try { | 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); | console.log(data); | ||||
const tempData = { | |||||
const userData = { | |||||
name: data.name, | name: data.name, | ||||
email: data.email, | |||||
email: '', | |||||
locked: false, | locked: false, | ||||
addAuthIds: data.addAuthIds || [], | addAuthIds: data.addAuthIds || [], | ||||
removeAuthIds: data.removeAuthIds || [], | 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"); | router.replace("/settings/staff"); | ||||
} catch (e) { | } catch (e) { | ||||
console.log(e); | console.log(e); | ||||
@@ -5,7 +5,7 @@ import EditUserLoading from "./EditUserLoading"; | |||||
import { useSearchParams } from "next/navigation"; | import { useSearchParams } from "next/navigation"; | ||||
import { fetchTeam, fetchTeamDetail } from "@/app/api/team"; | import { fetchTeam, fetchTeamDetail } from "@/app/api/team"; | ||||
import { fetchStaff } from "@/app/api/staff"; | import { fetchStaff } from "@/app/api/staff"; | ||||
import { fetchUser, fetchUserDetail } from "@/app/api/user"; | |||||
import { fetchPwRules, fetchUser, fetchUserDetail } from "@/app/api/user"; | |||||
interface SubComponents { | interface SubComponents { | ||||
Loading: typeof EditUserLoading; | Loading: typeof EditUserLoading; | ||||
@@ -17,10 +17,9 @@ interface Props { | |||||
const EditUserWrapper: React.FC<Props> & SubComponents = async ({ | const EditUserWrapper: React.FC<Props> & SubComponents = async ({ | ||||
// id | // id | ||||
}) => { | }) => { | ||||
// const users = await fetchUser() | |||||
// const userDetail = await fetchUserDetail(id) | |||||
const pwRule = await fetchPwRules() | |||||
return <EditUser /> | |||||
return <EditUser rules={pwRule} /> | |||||
}; | }; | ||||
EditUserWrapper.Loading = EditUserLoading; | EditUserWrapper.Loading = EditUserLoading; | ||||
@@ -47,12 +47,10 @@ const UserDetail: React.FC<Props> = ({ | |||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
label={t("email")} | |||||
label={t("password")} | |||||
fullWidth | fullWidth | ||||
{...register("email", { | |||||
required: "email required!", | |||||
})} | |||||
error={Boolean(errors.email)} | |||||
{...register("password")} | |||||
error={Boolean(errors.password)} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
</Grid> | </Grid> | ||||
@@ -24,11 +24,18 @@ const GenerateMonthlyWorkHoursReport: React.FC<Props> = ({ staffs }) => { | |||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | 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], | [t], | ||||
); | ); | ||||
@@ -38,9 +45,12 @@ return ( | |||||
<SearchBox | <SearchBox | ||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={async (query: any) => { | 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 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) { | if (response) { | ||||
downloadFile(new Uint8Array(response.blobValue), response.filename!!) | downloadFile(new Uint8Array(response.blobValue), response.filename!!) | ||||
} | } | ||||
@@ -41,6 +41,7 @@ const GenerateProjectCashFlowReport: React.FC<Props> = ({ projects }) => { | |||||
} | } | ||||
} | } | ||||
}} | }} | ||||
formType={"download"} | |||||
/> | /> | ||||
</> | </> | ||||
); | ); | ||||
@@ -21,13 +21,18 @@ import { ClientProjectResult} from "@/app/api/clientprojects"; | |||||
import { ConstructionOutlined } from "@mui/icons-material"; | import { ConstructionOutlined } from "@mui/icons-material"; | ||||
import ReactApexChart from "react-apexcharts"; | import ReactApexChart from "react-apexcharts"; | ||||
import { ApexOptions } from "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 }); | // const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); | ||||
interface Props { | 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 [activeTab, setActiveTab] = useState("financialSummary"); | ||||
const [SearchCriteria, setSearchCriteria] = React.useState({}); | const [SearchCriteria, setSearchCriteria] = React.useState({}); | ||||
const { t } = useTranslation("dashboard"); | const { t } = useTranslation("dashboard"); | ||||
@@ -56,6 +61,32 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
const [chartProjectName, setChartProjectName]:any[] = useState([]); | const [chartProjectName, setChartProjectName]:any[] = useState([]); | ||||
const [chartManhourConsumptionPercentage, setChartManhourConsumptionPercentage]:any[] = useState([]); | const [chartManhourConsumptionPercentage, setChartManhourConsumptionPercentage]:any[] = useState([]); | ||||
const color = ["#f57f90", "#94f7d6", "#87c5f5", "#ab95f5", "#fcd68b"]; | 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(() => { | useEffect(() => { | ||||
const projectName = [] | const projectName = [] | ||||
const manhourConsumptionPercentage = [] | const manhourConsumptionPercentage = [] | ||||
@@ -68,6 +99,12 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
setChartManhourConsumptionPercentage(manhourConsumptionPercentage) | setChartManhourConsumptionPercentage(manhourConsumptionPercentage) | ||||
}, [clientSubsidiaryProjectResult]); | }, [clientSubsidiaryProjectResult]); | ||||
useEffect(() => { | |||||
fetchData() | |||||
}, [customerId,subsidiaryId]); | |||||
const rows2 = [ | const rows2 = [ | ||||
{ | { | ||||
id: 1, | id: 1, | ||||
@@ -155,37 +192,37 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
></span> | ></span> | ||||
); | ); | ||||
}, | }, | ||||
flex: 0.1, | |||||
flex:0.1 | |||||
}, | }, | ||||
{ | { | ||||
id: "projectName", | id: "projectName", | ||||
field: "projectName", | field: "projectName", | ||||
headerName: "Project", | headerName: "Project", | ||||
flex: 1, | |||||
minWidth:300 | |||||
}, | }, | ||||
{ | { | ||||
id: "team", | id: "team", | ||||
field: "team", | field: "team", | ||||
headerName: "Team", | headerName: "Team", | ||||
flex: 0.8, | |||||
minWidth: 50 | |||||
}, | }, | ||||
{ | { | ||||
id: "teamLeader", | |||||
field: "teamLeader", | |||||
id: "teamLead", | |||||
field: "teamLead", | |||||
headerName: "Team Leader", | headerName: "Team Leader", | ||||
flex: 0.8, | |||||
minWidth: 70 | |||||
}, | }, | ||||
{ | { | ||||
id: "expectedStage", | id: "expectedStage", | ||||
field: "expectedStage", | field: "expectedStage", | ||||
headerName: "Expected Stage", | headerName: "Expected Stage", | ||||
flex: 1, | |||||
minWidth: 300 | |||||
}, | }, | ||||
{ | { | ||||
id: "budgetedManhour", | id: "budgetedManhour", | ||||
field: "budgetedManhour", | field: "budgetedManhour", | ||||
headerName: "Budgeted Manhour", | headerName: "Budgeted Manhour", | ||||
flex: 0.8, | |||||
minWidth: 70 | |||||
}, | }, | ||||
{ | { | ||||
id: "spentManhour", | id: "spentManhour", | ||||
@@ -201,7 +238,7 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
return <span>{params.row.spentManhour}</span>; | return <span>{params.row.spentManhour}</span>; | ||||
} | } | ||||
}, | }, | ||||
flex: 0.8, | |||||
minWidth: 70 | |||||
}, | }, | ||||
{ | { | ||||
id: "remainedManhour", | id: "remainedManhour", | ||||
@@ -216,13 +253,13 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
return <span>{params.row.remainedManhour}</span>; | return <span>{params.row.remainedManhour}</span>; | ||||
} | } | ||||
}, | }, | ||||
flex: 1, | |||||
minWidth: 70 | |||||
}, | }, | ||||
{ | { | ||||
id: "comingPaymentMilestone", | id: "comingPaymentMilestone", | ||||
field: "comingPaymentMilestone", | field: "comingPaymentMilestone", | ||||
headerName: "Coming Payment Milestone", | headerName: "Coming Payment Milestone", | ||||
flex: 1, | |||||
minWidth: 100 | |||||
}, | }, | ||||
{ | { | ||||
id: "alert", | id: "alert", | ||||
@@ -239,7 +276,7 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
return <span></span>; | return <span></span>; | ||||
} | } | ||||
}, | }, | ||||
flex: 0.2, | |||||
flex:0.1 | |||||
}, | }, | ||||
]; | ]; | ||||
const optionstest: ApexOptions = { | const optionstest: ApexOptions = { | ||||
@@ -463,7 +500,7 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
}; | }; | ||||
const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { | const handleSelectionChange = (newSelectionModel: GridRowSelectionModel) => { | ||||
const selectedRowsData:any = clientSubsidiaryProjectResult.filter((row) => | |||||
const selectedRowsData:any = clientSubsidiaryProjectResult.filter((row:any) => | |||||
newSelectionModel.includes(row.projectId), | newSelectionModel.includes(row.projectId), | ||||
); | ); | ||||
console.log(selectedRowsData); | console.log(selectedRowsData); | ||||
@@ -533,9 +570,9 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
<CardHeader className="text-slate-500" title="Project Resource Consumption" /> | <CardHeader className="text-slate-500" title="Project Resource Consumption" /> | ||||
<div style={{ display: "inline-block", width: "99%" }}> | <div style={{ display: "inline-block", width: "99%" }}> | ||||
<ReactApexChart | <ReactApexChart | ||||
options={optionstest} | |||||
series={optionstest.series} | |||||
type="line" | |||||
options={options} | |||||
series={options.series} | |||||
type="bar" | |||||
height={350} | height={350} | ||||
/> | /> | ||||
</div> | </div> | ||||
@@ -590,13 +627,13 @@ const ProgressByClient: React.FC<Props> = ({ clientSubsidiaryProjectResult }) => | |||||
Please select the project you want to check. | Please select the project you want to check. | ||||
</div> | </div> | ||||
)} | )} | ||||
{/* {percentageArray.length > 0 && ( | |||||
{percentageArray.length > 0 && ( | |||||
<ReactApexChart | <ReactApexChart | ||||
options={options2} | options={options2} | ||||
series={percentageArray} | series={percentageArray} | ||||
type="donut" | type="donut" | ||||
/> | /> | ||||
)} */} | |||||
)} | |||||
</Card> | </Card> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={12} md={12} lg={12}> | <Grid item xs={12} md={12} lg={12}> | ||||
@@ -1,7 +1,7 @@ | |||||
"use client"; | "use client"; | ||||
import { ProjectResult } from "@/app/api/projects"; | 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 SearchBox, { Criterion } from "../SearchBox"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
@@ -18,6 +18,7 @@ type SearchQuery = Partial<Omit<ClientProjectResult, "id">>; | |||||
type SearchParamNames = keyof SearchQuery; | type SearchParamNames = keyof SearchQuery; | ||||
const ProgressByClientSearch: React.FC<Props> = ({ clientProjects }) => { | const ProgressByClientSearch: React.FC<Props> = ({ clientProjects }) => { | ||||
const router = useRouter(); | |||||
const { t } = useTranslation("projects"); | const { t } = useTranslation("projects"); | ||||
const searchParams = useSearchParams() | const searchParams = useSearchParams() | ||||
// If project searching is done on the server-side, then no need for this. | // 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) => { | const onTaskClick = useCallback(async (clientProjectResult: ClientProjectResult) => { | ||||
try { | 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) { | } catch (error) { | ||||
console.error('Error fetching client subsidiary projects:', error); | console.error('Error fetching client subsidiary projects:', error); | ||||
} | } | ||||
@@ -86,7 +87,9 @@ const ProgressByClientSearch: React.FC<Props> = ({ clientProjects }) => { | |||||
items={filteredProjects} | items={filteredProjects} | ||||
columns={columns} | columns={columns} | ||||
/> | /> | ||||
<ProgressByClient clientSubsidiaryProjectResult={clientSubsidiaryProjectResult}/> | |||||
{/* {clientSubsidiaryProjectResult.length > 0 && ( | |||||
<ProgressByClient clientSubsidiaryProjectResult={clientSubsidiaryProjectResult} /> | |||||
)} */} | |||||
</> | </> | ||||
); | ); | ||||
}; | }; | ||||
@@ -1,12 +1,13 @@ | |||||
//src\components\LateStartReportGen\LateStartReportGen.tsx | //src\components\LateStartReportGen\LateStartReportGen.tsx | ||||
"use client"; | "use client"; | ||||
import React, { useMemo, useState } from "react"; | |||||
import React, { useEffect, useMemo, useState } from "react"; | |||||
import SearchBox, { Criterion } from "../ReportSearchBox"; | import SearchBox, { Criterion } from "../ReportSearchBox"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { LateStart } from "@/app/api/report"; | import { LateStart } from "@/app/api/report"; | ||||
//import { DownloadReportButton } from './DownloadReportButton'; | //import { DownloadReportButton } from './DownloadReportButton'; | ||||
// import axios from 'axios'; | // import axios from 'axios'; | ||||
import { apiPath } from '../../../auth/utils'; | import { apiPath } from '../../../auth/utils'; | ||||
import { fetchTeamCombo } from "@/app/api/team/actions"; | |||||
//import { GET_QC_CATEGORY_COMBO } from 'utils/ApiPathConst'; | //import { GET_QC_CATEGORY_COMBO } from 'utils/ApiPathConst'; | ||||
interface Props { | interface Props { | ||||
projects: LateStart[]; | projects: LateStart[]; | ||||
@@ -16,6 +17,9 @@ type SearchParamNames = keyof SearchQuery; | |||||
const ProgressByClientSearch: React.FC<Props> = ({ projects }) => { | const ProgressByClientSearch: React.FC<Props> = ({ projects }) => { | ||||
const { t } = useTranslation("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 [teamCombo, setteamCombo] = useState([]); | ||||
// const getteamCombo = () => { | // const getteamCombo = () => { | ||||
// axios.get(`${apiPath}${GET_QC_CATEGORY_COMBO}`) | // axios.get(`${apiPath}${GET_QC_CATEGORY_COMBO}`) | ||||
@@ -25,9 +29,32 @@ const ProgressByClientSearch: React.FC<Props> = ({ projects }) => { | |||||
// console.error('Error fetching data: ', error); | // 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( | 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: "Client", paramName: "client", type: "select", options: ["Cust A", "Cust B", "Cust C"] }, | ||||
{ | { | ||||
label: "Remained Date From", | label: "Remained Date From", | ||||
@@ -36,17 +63,17 @@ const ProgressByClientSearch: React.FC<Props> = ({ projects }) => { | |||||
type: "dateRange", | type: "dateRange", | ||||
}, | }, | ||||
], | ], | ||||
[t], | |||||
[t, teamCombo], | |||||
); | ); | ||||
return ( | return ( | ||||
<> | <> | ||||
<SearchBox | |||||
{!isLoading && <SearchBox | |||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={(query) => { | onSearch={(query) => { | ||||
console.log(query); | console.log(query); | ||||
}} | }} | ||||
/> | |||||
/>} | |||||
{/* <DownloadReportButton /> */} | {/* <DownloadReportButton /> */} | ||||
</> | </> | ||||
); | ); | ||||
@@ -11,7 +11,7 @@ interface SubComponents { | |||||
const LateStartReportGenWrapper: React.FC & SubComponents = async () => { | const LateStartReportGenWrapper: React.FC & SubComponents = async () => { | ||||
const clentprojects = await fetchProjectsLateStart(); | const clentprojects = await fetchProjectsLateStart(); | ||||
return <LateStartReportGen projects={clentprojects} />; | |||||
return <LateStartReportGen projects={clentprojects}/>; | |||||
}; | }; | ||||
LateStartReportGenWrapper.Loading = LateStartReportGenLoading; | LateStartReportGenWrapper.Loading = LateStartReportGenLoading; | ||||
@@ -22,7 +22,10 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | |||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | ||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
import { Box } from "@mui/material"; | import { Box } from "@mui/material"; | ||||
import { fetchLateStartReport } from "@/app/api/reports/actions"; | |||||
import * as XLSX from 'xlsx-js-style'; | 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'; | //import { DownloadReportButton } from '../LateStartReportGen/DownloadReportButton'; | ||||
interface BaseCriterion<T extends string> { | interface BaseCriterion<T extends string> { | ||||
@@ -113,25 +116,39 @@ function SearchBox<T extends string>({ | |||||
onSearch(inputs); | onSearch(inputs); | ||||
}; | }; | ||||
//fetchLateStartReport | |||||
const handleDownload = async () => { | const handleDownload = async () => { | ||||
try { | 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'); | const a = document.createElement('a'); | ||||
a.href = url; | 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); | document.body.appendChild(a); | ||||
a.click(); | a.click(); | ||||
a.remove(); | a.remove(); | ||||
// Optionally revoke the URL if you want to free up resources | |||||
window.URL.revokeObjectURL(url); | |||||
} catch (error) { | } catch (error) { | ||||
console.error('Error downloading the file: ', error); | console.error('Error downloading the file: ', error); | ||||
} | } | ||||
@@ -15,12 +15,14 @@ import CardActions from "@mui/material/CardActions"; | |||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
import RestartAlt from "@mui/icons-material/RestartAlt"; | import RestartAlt from "@mui/icons-material/RestartAlt"; | ||||
import Search from "@mui/icons-material/Search"; | import Search from "@mui/icons-material/Search"; | ||||
import FileDownload from '@mui/icons-material/FileDownload'; | |||||
import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
import "dayjs/locale/zh-hk"; | import "dayjs/locale/zh-hk"; | ||||
import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | import { DatePicker } from "@mui/x-date-pickers/DatePicker"; | ||||
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; | ||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | ||||
import { Box } from "@mui/material"; | import { Box } from "@mui/material"; | ||||
import { DateCalendar } from "@mui/x-date-pickers"; | |||||
interface BaseCriterion<T extends string> { | interface BaseCriterion<T extends string> { | ||||
label: string; | label: string; | ||||
@@ -43,21 +45,28 @@ interface DateRangeCriterion<T extends string> extends BaseCriterion<T> { | |||||
type: "dateRange"; | type: "dateRange"; | ||||
} | } | ||||
interface MonthYearCriterion<T extends string> extends BaseCriterion<T> { | |||||
type: "monthYear"; | |||||
} | |||||
export type Criterion<T extends string> = | export type Criterion<T extends string> = | ||||
| TextCriterion<T> | | TextCriterion<T> | ||||
| SelectCriterion<T> | | SelectCriterion<T> | ||||
| DateRangeCriterion<T>; | |||||
| DateRangeCriterion<T> | |||||
| MonthYearCriterion<T>; | |||||
interface Props<T extends string> { | interface Props<T extends string> { | ||||
criteria: Criterion<T>[]; | criteria: Criterion<T>[]; | ||||
onSearch: (inputs: Record<T, string>) => void; | onSearch: (inputs: Record<T, string>) => void; | ||||
onReset?: () => void; | onReset?: () => void; | ||||
formType?: String, | |||||
} | } | ||||
function SearchBox<T extends string>({ | function SearchBox<T extends string>({ | ||||
criteria, | criteria, | ||||
onSearch, | onSearch, | ||||
onReset, | onReset, | ||||
formType, | |||||
}: Props<T>) { | }: Props<T>) { | ||||
const { t } = useTranslation("common"); | const { t } = useTranslation("common"); | ||||
const defaultInputs = useMemo( | const defaultInputs = useMemo( | ||||
@@ -66,19 +75,19 @@ function SearchBox<T extends string>({ | |||||
(acc, c) => { | (acc, c) => { | ||||
return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; | return { ...acc, [c.paramName]: c.type === "select" ? "All" : "" }; | ||||
}, | }, | ||||
{} as Record<T, string>, | |||||
{} as Record<T, string> | |||||
), | ), | ||||
[criteria], | |||||
[criteria] | |||||
); | ); | ||||
const [inputs, setInputs] = useState(defaultInputs); | const [inputs, setInputs] = useState(defaultInputs); | ||||
const makeInputChangeHandler = useCallback( | const makeInputChangeHandler = useCallback( | ||||
(paramName: T): React.ChangeEventHandler<HTMLInputElement> => { | (paramName: T): React.ChangeEventHandler<HTMLInputElement> => { | ||||
return (e) => { | return (e) => { | ||||
setInputs((i) => ({ ...i, [paramName]: e.target.value })); | setInputs((i) => ({ ...i, [paramName]: e.target.value })); | ||||
}; | }; | ||||
}, | }, | ||||
[], | |||||
[] | |||||
); | ); | ||||
const makeSelectChangeHandler = useCallback((paramName: T) => { | 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) => { | const makeDateToChangeHandler = useCallback((paramName: T) => { | ||||
return (e: any) => { | return (e: any) => { | ||||
setInputs((i) => ({ | setInputs((i) => ({ | ||||
@@ -135,7 +151,9 @@ function SearchBox<T extends string>({ | |||||
onChange={makeSelectChangeHandler(c.paramName)} | onChange={makeSelectChangeHandler(c.paramName)} | ||||
value={inputs[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) => ( | {c.options.map((option, index) => ( | ||||
<MenuItem key={`${option}-${index}`} value={option}> | <MenuItem key={`${option}-${index}`} value={option}> | ||||
{t(option)} | {t(option)} | ||||
@@ -144,6 +162,26 @@ function SearchBox<T extends string>({ | |||||
</Select> | </Select> | ||||
</FormControl> | </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" && ( | {c.type === "dateRange" && ( | ||||
<LocalizationProvider | <LocalizationProvider | ||||
dateAdapter={AdapterDayjs} | dateAdapter={AdapterDayjs} | ||||
@@ -155,7 +193,11 @@ function SearchBox<T extends string>({ | |||||
<DatePicker | <DatePicker | ||||
label={c.label} | label={c.label} | ||||
onChange={makeDateChangeHandler(c.paramName)} | onChange={makeDateChangeHandler(c.paramName)} | ||||
value={inputs[c.paramName] ? dayjs(inputs[c.paramName]) : null} | |||||
value={ | |||||
inputs[c.paramName] | |||||
? dayjs(inputs[c.paramName]) | |||||
: null | |||||
} | |||||
/> | /> | ||||
</FormControl> | </FormControl> | ||||
<Box | <Box | ||||
@@ -170,7 +212,11 @@ function SearchBox<T extends string>({ | |||||
<DatePicker | <DatePicker | ||||
label={c.label2} | label={c.label2} | ||||
onChange={makeDateToChangeHandler(c.paramName)} | 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> | </FormControl> | ||||
</Box> | </Box> | ||||
@@ -190,10 +236,10 @@ function SearchBox<T extends string>({ | |||||
</Button> | </Button> | ||||
<Button | <Button | ||||
variant="outlined" | variant="outlined" | ||||
startIcon={<Search />} | |||||
startIcon={(formType === "download" && <FileDownload />) || <Search />} | |||||
onClick={handleSearch} | onClick={handleSearch} | ||||
> | > | ||||
{t("Search")} | |||||
{(formType === "download" && t("Download")) || t("Search")} | |||||
</Button> | </Button> | ||||
</CardActions> | </CardActions> | ||||
</CardContent> | </CardContent> | ||||
@@ -19,6 +19,7 @@ | |||||
"Details": "Details", | "Details": "Details", | ||||
"Delete": "Delete", | "Delete": "Delete", | ||||
"Download": "Download", | |||||
"Search": "Search", | "Search": "Search", | ||||
"Search Criteria": "Search Criteria", | "Search Criteria": "Search Criteria", | ||||
"Cancel": "Cancel", | "Cancel": "Cancel", | ||||
@@ -17,6 +17,7 @@ | |||||
"Details": "詳情", | "Details": "詳情", | ||||
"Delete": "刪除", | "Delete": "刪除", | ||||
"Download": "下載", | |||||
"Search": "搜尋", | "Search": "搜尋", | ||||
"Search Criteria": "搜尋條件", | "Search Criteria": "搜尋條件", | ||||
"Cancel": "取消", | "Cancel": "取消", | ||||