| @@ -0,0 +1,28 @@ | |||||
| import { Metadata } from "next"; | |||||
| import { Suspense } from "react"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import GenerateMonthlyWorkHoursReport from "@/components/GenerateMonthlyWorkHoursReport"; | |||||
| import { Typography } from "@mui/material"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Staff Monthly Work Hours Analysis Report", | |||||
| }; | |||||
| const StaffMonthlyWorkHoursAnalysisReport: React.FC = async () => { | |||||
| const { t } = await getServerI18n("User Group"); | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Staff Monthly Work Hours Analysis Report")} | |||||
| </Typography> | |||||
| <I18nProvider namespaces={["report", "common"]}> | |||||
| <Suspense fallback={<GenerateMonthlyWorkHoursReport.Loading />}> | |||||
| <GenerateMonthlyWorkHoursReport /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default StaffMonthlyWorkHoursAnalysisReport; | |||||
| @@ -7,21 +7,28 @@ import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import EditSkill from "@/components/EditSkill"; | import EditSkill from "@/components/EditSkill"; | ||||
| import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
| import { fetchSkill } from "@/app/api/skill"; | |||||
| export interface searchParamsProps { | |||||
| searchParams: { [key: string]: string | string[] | undefined }; | |||||
| } | |||||
| const EditSkillPage: React.FC = async () => { | |||||
| const EditSkillPage: React.FC<searchParamsProps> = async ({ | |||||
| searchParams, | |||||
| }) => { | |||||
| console.log(searchParams.id) | |||||
| const { t } = await getServerI18n("staff"); | const { t } = await getServerI18n("staff"); | ||||
| return ( | return ( | ||||
| <> | |||||
| <Typography variant="h4">{t("Edit Skill")}</Typography> | |||||
| <I18nProvider namespaces={["team", "common"]}> | |||||
| <Suspense fallback={<EditSkill.Loading />}> | |||||
| <EditSkill /> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| {/* <EditStaff /> */} | |||||
| </> | |||||
| <> | |||||
| <Typography variant="h4">{t("Edit Skill")}</Typography> | |||||
| <I18nProvider namespaces={["team", "common"]}> | |||||
| <Suspense fallback={<EditSkill.Loading />}> | |||||
| <EditSkill id={parseInt(searchParams.id as string)}/> | |||||
| </Suspense> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | ); | ||||
| }; | }; | ||||
| @@ -4,16 +4,20 @@ import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import EditUser from "@/components/EditUser"; | import EditUser from "@/components/EditUser"; | ||||
| import { Typography } from "@mui/material"; | import { Typography } from "@mui/material"; | ||||
| import { Suspense } from "react"; | import { Suspense } from "react"; | ||||
| import { searchParamsProps } from "../../skill/edit/page"; | |||||
| import { preloadUser } from "@/app/api/user"; | |||||
| const User: React.FC = async () => { | |||||
| const User: React.FC<searchParamsProps> = async ({ | |||||
| searchParams | |||||
| }) => { | |||||
| const { t } = await getServerI18n("user"); | const { t } = await getServerI18n("user"); | ||||
| preloadUser() | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Typography variant="h4">{t("Edit User")}</Typography> | <Typography variant="h4">{t("Edit User")}</Typography> | ||||
| <I18nProvider namespaces={["user", "common"]}> | <I18nProvider namespaces={["user", "common"]}> | ||||
| <Suspense fallback={<EditUser.Loading />}> | <Suspense fallback={<EditUser.Loading />}> | ||||
| <EditUser /> | |||||
| <EditUser id={parseInt(searchParams.id as string)}/> | |||||
| </Suspense> | </Suspense> | ||||
| </I18nProvider> | </I18nProvider> | ||||
| </> | </> | ||||
| @@ -6,15 +6,25 @@ import { I18nProvider } from "@/i18n"; | |||||
| // import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; | // import EditStaffWrapper from "@/components/EditStaff/EditStaffWrapper"; | ||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import EditUser from "@/components/EditUser"; | import EditUser from "@/components/EditUser"; | ||||
| import { useRouter } from 'next/router'; | |||||
| import { getServerSideProps } from "next/dist/build/templates/pages"; | |||||
| import { searchParamsProps } from "../../skill/edit/page"; | |||||
| import { preloadUserDetail } from "@/app/api/user"; | |||||
| const EditUserPage: React.FC = () => { | |||||
| const EditUserPage: React.FC<searchParamsProps> = async ({ | |||||
| searchParams | |||||
| }) => { | |||||
| const id = parseInt(searchParams.id as string) | |||||
| preloadUserDetail(id) | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <I18nProvider namespaces={["team", "common"]}> | <I18nProvider namespaces={["team", "common"]}> | ||||
| <Suspense fallback={<EditUser.Loading />}> | <Suspense fallback={<EditUser.Loading />}> | ||||
| <EditUser /> | |||||
| <EditUser | |||||
| // id={id} | |||||
| /> | |||||
| </Suspense> | </Suspense> | ||||
| </I18nProvider> | </I18nProvider> | ||||
| </> | </> | ||||
| @@ -29,7 +29,7 @@ export interface record { | |||||
| records: auth[]; | records: auth[]; | ||||
| } | } | ||||
| export const fetchAuth = cache(async (target: string, id?: number) => { | |||||
| export const fetchAuth = cache(async (target: string, id?: number ) => { | |||||
| return serverFetchJson<record>(`${BASE_API_URL}/group/auth/${target}/${id ?? 0}`, { | return serverFetchJson<record>(`${BASE_API_URL}/group/auth/${target}/${id ?? 0}`, { | ||||
| next: { tags: ["auth"] }, | next: { tags: ["auth"] }, | ||||
| }); | }); | ||||
| @@ -1,7 +1,7 @@ | |||||
| "use server"; | "use server"; | ||||
| import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; | import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; | ||||
| import { ProjectCashFlowReportRequest } from "."; | |||||
| import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest } from "."; | |||||
| import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
| export interface FileResponse { | export interface FileResponse { | ||||
| @@ -19,5 +19,18 @@ export const fetchProjectCashFlowReport = async (data: ProjectCashFlowReportRequ | |||||
| }, | }, | ||||
| ); | ); | ||||
| return reportBlob | |||||
| }; | |||||
| export const fetchMonthlyWorkHoursReport = async (data: MonthlyWorkHoursReportRequest) => { | |||||
| const reportBlob = await serverFetchBlob<FileResponse>( | |||||
| `${BASE_API_URL}/reports/StaffMonthlyWorkHourAnalysisReport`, | |||||
| { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }, | |||||
| ); | |||||
| return reportBlob | return reportBlob | ||||
| }; | }; | ||||
| @@ -1,3 +1,5 @@ | |||||
| import { records } from "../staff/actions"; | |||||
| // - Project Cash Flow Report | // - Project Cash Flow Report | ||||
| export interface ProjectCashFlowReportFilter { | export interface ProjectCashFlowReportFilter { | ||||
| project: string[]; | project: string[]; | ||||
| @@ -5,4 +7,16 @@ export interface ProjectCashFlowReportFilter { | |||||
| export interface ProjectCashFlowReportRequest { | export interface ProjectCashFlowReportRequest { | ||||
| projectId: number; | projectId: number; | ||||
| } | |||||
| } | |||||
| // - Monthly Work Hours Report | |||||
| export interface MonthlyWorkHoursReportFilter { | |||||
| staff: string[]; | |||||
| date: any; | |||||
| } | |||||
| export interface MonthlyWorkHoursReportRequest { | |||||
| id: number; | |||||
| yearMonth: string; | |||||
| } | |||||
| @@ -19,4 +19,10 @@ export interface SkillResult { | |||||
| return serverFetchJson<SkillResult[]>(`${BASE_API_URL}/skill`, { | return serverFetchJson<SkillResult[]>(`${BASE_API_URL}/skill`, { | ||||
| next: { tags: ["sill"] }, | next: { tags: ["sill"] }, | ||||
| }); | }); | ||||
| }); | |||||
| export const fetchSkillDetail = cache(async (id: number) => { | |||||
| return serverFetchJson<SkillResult[]>(`${BASE_API_URL}/skill/${id}`, { | |||||
| next: { tags: ["sill"] }, | |||||
| }); | |||||
| }); | }); | ||||
| @@ -39,9 +39,9 @@ export interface CreateStaffInputs { | |||||
| name: string; | name: string; | ||||
| // team: Team[]; | // team: Team[]; | ||||
| } | } | ||||
| export interface Staff4TransferList { | |||||
| records: records[]; | |||||
| } | |||||
| // export interface Staff4TransferList { | |||||
| // records: records[]; | |||||
| // } | |||||
| export const saveStaff = async (data: CreateStaffInputs) => { | export const saveStaff = async (data: CreateStaffInputs) => { | ||||
| return serverFetchJson(`${BASE_API_URL}/staffs/save`, { | return serverFetchJson(`${BASE_API_URL}/staffs/save`, { | ||||
| @@ -79,7 +79,7 @@ export const fetchStaffEdit = cache(async (id: number) => { | |||||
| // }; | // }; | ||||
| export const fetchStaffCombo = cache(async () => { | export const fetchStaffCombo = cache(async () => { | ||||
| return serverFetchJson<Staff4TransferList>(`${BASE_API_URL}/staffs/combo`, { | |||||
| return serverFetchJson<records[]>(`${BASE_API_URL}/staffs/combo`, { | |||||
| next: { tags: ["staffs"] }, | next: { tags: ["staffs"] }, | ||||
| }); | }); | ||||
| }); | }); | ||||
| @@ -19,7 +19,6 @@ export interface PasswordInputs { | |||||
| newPasswordCheck: string; | newPasswordCheck: string; | ||||
| } | } | ||||
| export const fetchUserDetails = cache(async (id: number) => { | export const fetchUserDetails = cache(async (id: number) => { | ||||
| return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, { | return serverFetchJson<UserDetail>(`${BASE_API_URL}/user/${id}`, { | ||||
| next: { tags: ["user"] }, | next: { tags: ["user"] }, | ||||
| @@ -38,8 +38,18 @@ export interface UserDetail { | |||||
| fetchUser(); | fetchUser(); | ||||
| }; | }; | ||||
| export const preloadUserDetail = (id: number) => { | |||||
| fetchUserDetail(id); | |||||
| }; | |||||
| export const fetchUser = cache(async () => { | export const fetchUser = cache(async () => { | ||||
| return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user`, { | return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user`, { | ||||
| next: { tags: ["user"] }, | next: { tags: ["user"] }, | ||||
| }); | }); | ||||
| }); | |||||
| export const fetchUserDetail = cache(async (id: number) => { | |||||
| return serverFetchJson<UserResult[]>(`${BASE_API_URL}/user/${id}`, { | |||||
| next: { tags: ["user"] }, | |||||
| }); | |||||
| }); | }); | ||||
| @@ -11,7 +11,7 @@ interface SubComponents { | |||||
| } | } | ||||
| const CreateGroupWrapper: React.FC & SubComponents = async () => { | const CreateGroupWrapper: React.FC & SubComponents = async () => { | ||||
| const records = await fetchAuth() | |||||
| const records = await fetchAuth("group") | |||||
| const users = await fetchUser() | const users = await fetchUser() | ||||
| console.log(users) | console.log(users) | ||||
| const auth = records.records as auth[] | const auth = records.records as auth[] | ||||
| @@ -13,7 +13,6 @@ import { | |||||
| } from "react-hook-form"; | } from "react-hook-form"; | ||||
| // import CreateTeamForm from "../CreateTeamForm"; | // import CreateTeamForm from "../CreateTeamForm"; | ||||
| import { CreateTeamInputs } from "@/app/api/team/actions"; | import { CreateTeamInputs } from "@/app/api/team/actions"; | ||||
| // import { Staff4TransferList, fetchStaffCombo } from "@/app/api/staff/actions"; | |||||
| import { StaffResult } from "@/app/api/staff"; | import { StaffResult } from "@/app/api/staff"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
| import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; | import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; | ||||
| @@ -3,15 +3,18 @@ import EditSkill from "./EditSkill"; | |||||
| import EditSkillLoading from "./EditSkillLoading"; | import EditSkillLoading from "./EditSkillLoading"; | ||||
| import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; | ||||
| import { useSearchParams } from "next/navigation"; | import { useSearchParams } from "next/navigation"; | ||||
| import { fetchSkill } from "@/app/api/skill"; | |||||
| import { fetchSkill, fetchSkillDetail } from "@/app/api/skill"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof EditSkillLoading; | Loading: typeof EditSkillLoading; | ||||
| } | } | ||||
| const EditSkillWrapper: React.FC & SubComponents = async () => { | |||||
| const skills = await fetchSkill() | |||||
| console.log(skills) | |||||
| interface Props { | |||||
| id: number | |||||
| } | |||||
| const EditSkillWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | |||||
| const skills = await fetchSkillDetail(id) | |||||
| return <EditSkill skills={skills}/>; | return <EditSkill skills={skills}/>; | ||||
| }; | }; | ||||
| @@ -12,7 +12,6 @@ import { | |||||
| useFormContext, | useFormContext, | ||||
| } from "react-hook-form"; | } from "react-hook-form"; | ||||
| import { CreateTeamInputs } from "@/app/api/team/actions"; | import { CreateTeamInputs } from "@/app/api/team/actions"; | ||||
| import { Staff4TransferList, fetchStaffCombo } from "@/app/api/staff/actions"; | |||||
| import { StaffResult, StaffTeamTable } from "@/app/api/staff"; | import { StaffResult, StaffTeamTable } from "@/app/api/staff"; | ||||
| import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
| import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; | import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; | ||||
| @@ -5,15 +5,20 @@ 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 } from "@/app/api/user"; | |||||
| import { fetchUser, fetchUserDetail } from "@/app/api/user"; | |||||
| interface SubComponents { | interface SubComponents { | ||||
| Loading: typeof EditUserLoading; | Loading: typeof EditUserLoading; | ||||
| } | } | ||||
| const EditUserWrapper: React.FC & SubComponents = async () => { | |||||
| const users = await fetchUser() | |||||
| console.log(users) | |||||
| interface Props { | |||||
| // id: number | |||||
| } | |||||
| const EditUserWrapper: React.FC<Props> & SubComponents = async ({ | |||||
| // id | |||||
| }) => { | |||||
| // const users = await fetchUser() | |||||
| // const userDetail = await fetchUserDetail(id) | |||||
| return <EditUser /> | return <EditUser /> | ||||
| }; | }; | ||||
| @@ -0,0 +1,55 @@ | |||||
| "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 } from "@/app/api/reports/actions"; | |||||
| import { downloadFile } from "@/app/utils/commonUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { MonthlyWorkHoursReportFilter } from "@/app/api/reports"; | |||||
| import { records } from "@/app/api/staff/actions"; | |||||
| import { StaffResult } from "@/app/api/staff"; | |||||
| interface Props { | |||||
| staffs: StaffResult[] | |||||
| } | |||||
| type SearchQuery = Partial<Omit<MonthlyWorkHoursReportFilter, "id">>; | |||||
| type SearchParamNames = keyof SearchQuery; | |||||
| const GenerateMonthlyWorkHoursReport: React.FC<Props> = ({ staffs }) => { | |||||
| const { t } = useTranslation(); | |||||
| const staffCombo = staffs.map(staff => `${staff.name} - ${staff.staffId}`) | |||||
| console.log(staffs) | |||||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||||
| () => [ | |||||
| { label: t("Staff"), | |||||
| paramName: "staff", | |||||
| type: "select", | |||||
| options: staffCombo, | |||||
| needAll: false}, | |||||
| ], | |||||
| [t], | |||||
| ); | |||||
| return ( | |||||
| <> | |||||
| <SearchBox | |||||
| criteria={searchCriteria} | |||||
| onSearch={async (query: any) => { | |||||
| if (query.staff.length > 0 && query.staff.toLocaleLowerCase() !== "all") { | |||||
| const index = staffCombo.findIndex(staff => staff === query.staff) | |||||
| const response = await fetchMonthlyWorkHoursReport({ id: staffs[index].id, yearMonth: "2023-03" }) | |||||
| if (response) { | |||||
| downloadFile(new Uint8Array(response.blobValue), response.filename!!) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| /> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default GenerateMonthlyWorkHoursReport | |||||
| @@ -0,0 +1,38 @@ | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Skeleton from "@mui/material/Skeleton"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import React from "react"; | |||||
| // Can make this nicer | |||||
| export const GenerateMonthlyWorkHoursReportLoading: React.FC = () => { | |||||
| return ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default GenerateMonthlyWorkHoursReportLoading; | |||||
| @@ -0,0 +1,19 @@ | |||||
| import React from "react"; | |||||
| import GenerateMonthlyWorkHoursReportLoading from "./GenerateMonthlyWorkHoursReportLoading"; | |||||
| import { fetchProjects } from "@/app/api/projects"; | |||||
| import GenerateMonthlyWorkHoursReport from "./GenerateMonthlyWorkHoursReport"; | |||||
| import { fetchStaff } from "@/app/api/staff"; | |||||
| interface SubComponents { | |||||
| Loading: typeof GenerateMonthlyWorkHoursReportLoading; | |||||
| } | |||||
| const GenerateMonthlyWorkHoursReportWrapper: React.FC & SubComponents = async () => { | |||||
| const staffs = await fetchStaff(); | |||||
| return <GenerateMonthlyWorkHoursReport staffs={staffs}/>; | |||||
| }; | |||||
| GenerateMonthlyWorkHoursReportWrapper.Loading = GenerateMonthlyWorkHoursReportLoading; | |||||
| export default GenerateMonthlyWorkHoursReportWrapper; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./GenerateMonthlyWorkHoursReportWrapper"; | |||||
| @@ -128,6 +128,7 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => { | |||||
| {icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, | {icon: <Analytics />, label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, | ||||
| {icon: <Analytics />, label:"Financial Status Report", path: "/analytics/FinancialStatusReport"}, | {icon: <Analytics />, label:"Financial Status Report", path: "/analytics/FinancialStatusReport"}, | ||||
| {icon: <Analytics />, label:"Project Cash Flow Report", path: "/analytics/ProjectCashFlowReport"}, | {icon: <Analytics />, label:"Project Cash Flow Report", path: "/analytics/ProjectCashFlowReport"}, | ||||
| {icon: <Analytics />, label:"Staff Monthly Work Hours Analysis Report", path: "/analytics/StaffMonthlyWorkHoursAnalysisReport"}, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -142,7 +143,7 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => { | |||||
| { icon: <Position />, label: "Position", path: "/settings/position" }, | { icon: <Position />, label: "Position", path: "/settings/position" }, | ||||
| { icon: <Salary />, label: "Salary", path: "/settings/salary" }, | { icon: <Salary />, label: "Salary", path: "/settings/salary" }, | ||||
| { icon: <Team />, label: "Team", path: "/settings/team" }, | { icon: <Team />, label: "Team", path: "/settings/team" }, | ||||
| { icon: <ManageAccountsIcon />, label: "User", path: "/settings/user" }, | |||||
| // { icon: <ManageAccountsIcon />, label: "User", path: "/settings/user" }, | |||||
| { icon: <ManageAccountsIcon />, label: "User Group", path: "/settings/group" }, | { icon: <ManageAccountsIcon />, label: "User Group", path: "/settings/group" }, | ||||
| { icon: <Holiday />, label: "Holiday", path: "/settings/holiday" }, | { icon: <Holiday />, label: "Holiday", path: "/settings/holiday" }, | ||||
| ], | ], | ||||