From afe7cac746466dbcd2484f1aa6d3bad331e72354 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Mon, 29 Apr 2024 12:19:37 +0800 Subject: [PATCH 01/11] master pages --- src/app/(main)/settings/skill/create/page.tsx | 48 +++++ src/app/(main)/settings/skill/page.tsx | 50 +++++ src/app/(main)/settings/user/page.tsx | 54 ++++++ src/app/api/skill/actions.ts | 18 +- src/app/api/skill/index.ts | 22 +++ src/app/api/staff/actions.ts | 6 +- src/app/api/team/actions.ts | 7 +- src/app/api/user/actions.ts | 27 +++ src/app/api/user/index.ts | 43 +++++ src/components/CreateSkill/CreateSkill.tsx | 122 ++++++++++++ .../CreateSkill/CreateSkillLoading.tsx | 40 ++++ .../CreateSkill/CreateSkillWrapper.tsx | 19 ++ src/components/CreateSkill/SkillInfo.tsx | 90 +++++++++ src/components/CreateSkill/index.ts | 1 + src/components/CreateTeam/CreateTeam.tsx | 2 +- src/components/CreateTeam/StaffAllocation.tsx | 174 ++++++++++-------- .../NavigationContent/NavigationContent.tsx | 4 + src/components/SkillSearch/SkillSearch.tsx | 96 ++++++++++ .../SkillSearch/SkillSearchLoading.tsx | 40 ++++ .../SkillSearch/SkillSearchWrapper.tsx | 27 +++ src/components/SkillSearch/index.ts | 1 + .../StaffSearch/ConfirmDeleteModal.tsx | 106 ----------- src/components/StaffSearch/StaffSearch.tsx | 79 +++----- .../TeamSearch/ConfirmDeleteModal.tsx | 105 ----------- src/components/TeamSearch/TeamSearch.tsx | 169 ++++++++--------- src/components/UserSearch/UserSearch.tsx | 98 ++++++++++ .../UserSearch/UserSearchLoading.tsx | 40 ++++ .../UserSearch/UserSearchWrapper.tsx | 19 ++ src/components/UserSearch/index.ts | 1 + 29 files changed, 1062 insertions(+), 446 deletions(-) create mode 100644 src/app/(main)/settings/skill/create/page.tsx create mode 100644 src/app/(main)/settings/skill/page.tsx create mode 100644 src/app/(main)/settings/user/page.tsx create mode 100644 src/app/api/skill/index.ts create mode 100644 src/app/api/user/actions.ts create mode 100644 src/app/api/user/index.ts create mode 100644 src/components/CreateSkill/CreateSkill.tsx create mode 100644 src/components/CreateSkill/CreateSkillLoading.tsx create mode 100644 src/components/CreateSkill/CreateSkillWrapper.tsx create mode 100644 src/components/CreateSkill/SkillInfo.tsx create mode 100644 src/components/CreateSkill/index.ts create mode 100644 src/components/SkillSearch/SkillSearch.tsx create mode 100644 src/components/SkillSearch/SkillSearchLoading.tsx create mode 100644 src/components/SkillSearch/SkillSearchWrapper.tsx create mode 100644 src/components/SkillSearch/index.ts delete mode 100644 src/components/StaffSearch/ConfirmDeleteModal.tsx delete mode 100644 src/components/TeamSearch/ConfirmDeleteModal.tsx create mode 100644 src/components/UserSearch/UserSearch.tsx create mode 100644 src/components/UserSearch/UserSearchLoading.tsx create mode 100644 src/components/UserSearch/UserSearchWrapper.tsx create mode 100644 src/components/UserSearch/index.ts diff --git a/src/app/(main)/settings/skill/create/page.tsx b/src/app/(main)/settings/skill/create/page.tsx new file mode 100644 index 0000000..c98f993 --- /dev/null +++ b/src/app/(main)/settings/skill/create/page.tsx @@ -0,0 +1,48 @@ +// 'use client'; +import { I18nProvider, getServerI18n } from "@/i18n"; +import CustomInputForm from "@/components/CustomInputForm"; +import Check from "@mui/icons-material/Check"; +import Close from "@mui/icons-material/Close"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Tab from "@mui/material/Tab"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Task, TaskTemplate } from "@/app/api/tasks"; +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions"; +import { Error } from "@mui/icons-material"; +import { ProjectCategory } from "@/app/api/projects"; +import { Grid, Typography } from "@mui/material"; +import CreateStaffForm from "@/components/CreateStaff/CreateStaff"; +import CreateSkill from "@/components/CreateSkill"; + +// const Title = ["title1", "title2"]; + +const CreateStaff: React.FC = async () => { + const { t } = await getServerI18n("staff"); + + const title = ['', t('Additional Info')] + // const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$") + // console.log(regex) + + return ( + <> + {t("Create Skill")} + + + + + ); +}; + +export default CreateStaff; diff --git a/src/app/(main)/settings/skill/page.tsx b/src/app/(main)/settings/skill/page.tsx new file mode 100644 index 0000000..f263c87 --- /dev/null +++ b/src/app/(main)/settings/skill/page.tsx @@ -0,0 +1,50 @@ +import { preloadClaims } from "@/app/api/claims"; +// import { preloadSkill, preloadTeamLeads } from "@/app/api/staff"; +import SkillSearch from "@/components/SkillSearch"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import Link from "next/link"; +import { Suspense } from "react"; + +export const metadata: Metadata = { + title: "Skill", +}; + +const Skill: React.FC = async () => { + const { t } = await getServerI18n("skill"); +// preloadTeamLeads(); +// preloadSkill(); + return ( + <> + + + {t("Skill")} + + + + + }> + + + + + ); +}; + +export default Skill; diff --git a/src/app/(main)/settings/user/page.tsx b/src/app/(main)/settings/user/page.tsx new file mode 100644 index 0000000..95973ab --- /dev/null +++ b/src/app/(main)/settings/user/page.tsx @@ -0,0 +1,54 @@ +import { preloadClaims } from "@/app/api/claims"; +import { preloadStaff, preloadTeamLeads } from "@/app/api/staff"; +import StaffSearch from "@/components/StaffSearch"; +import TeamSearch from "@/components/TeamSearch"; +import UserSearch from "@/components/UserSearch"; +import { I18nProvider, getServerI18n } from "@/i18n"; +import Add from "@mui/icons-material/Add"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import Link from "next/link"; +import { Suspense } from "react"; + + +export const metadata: Metadata = { + title: "User", + }; + + + const User: React.FC = async () => { + const { t } = await getServerI18n("User"); + // preloadTeamLeads(); + // preloadStaff(); + return ( + <> + + + {t("User")} + + + + + }> + + + + + ); + }; + + export default User; \ No newline at end of file diff --git a/src/app/api/skill/actions.ts b/src/app/api/skill/actions.ts index eda7f39..6a0deca 100644 --- a/src/app/api/skill/actions.ts +++ b/src/app/api/skill/actions.ts @@ -5,6 +5,13 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; +export interface CreateSkillInputs { + id?: number; + name: String; + code: String; + description: String; +} + export interface comboProp { id: any; label: string; @@ -18,4 +25,13 @@ export const fetchSkillCombo = cache(async () => { return serverFetchJson(`${BASE_API_URL}/skill/combo`, { next: { tags: ["skill"] }, }); - }); \ No newline at end of file + }); + + +export const saveSkill = async (data: CreateSkillInputs) => { + return serverFetchJson(`${BASE_API_URL}/skill/save`, { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/skill/index.ts b/src/app/api/skill/index.ts new file mode 100644 index 0000000..cf6ebec --- /dev/null +++ b/src/app/api/skill/index.ts @@ -0,0 +1,22 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + +export interface SkillResult { + action: any; + id: number; + name: string; + description: string; + code: string; + } + + export const preloadSkill = () => { + fetchSkill(); + }; + + export const fetchSkill = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/skill`, { + next: { tags: ["sill"] }, + }); + }); \ No newline at end of file diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts index 9416d2d..a2235d6 100644 --- a/src/app/api/staff/actions.ts +++ b/src/app/api/staff/actions.ts @@ -1,5 +1,5 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { StaffResult, data } from "."; import { cache } from "react"; @@ -59,8 +59,8 @@ export const testing = async (data: CreateStaffInputs) => { }); }; -export const deleteStaff = async (data: StaffResult) => { - return serverFetchJson(`${BASE_API_URL}/staffs/delete/${data.id}`, { +export const deleteStaff = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/staffs/delete/${id}`, { method: "DELETE", // body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, diff --git a/src/app/api/team/actions.ts b/src/app/api/team/actions.ts index 28496d0..47e1a82 100644 --- a/src/app/api/team/actions.ts +++ b/src/app/api/team/actions.ts @@ -1,5 +1,5 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; import { TeamResult } from "."; @@ -53,10 +53,9 @@ export const saveTeam = async (data: CreateTeamInputs) => { }; -export const deleteTeam = async (data: TeamResult) => { - return serverFetchJson(`${BASE_API_URL}/team/delete/${data.id}`, { +export const deleteTeam = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/team/delete/${id}`, { method: "DELETE", - // body: JSON.stringify(data), headers: { "Content-Type": "application/json" }, }); }; diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts new file mode 100644 index 0000000..5df734a --- /dev/null +++ b/src/app/api/user/actions.ts @@ -0,0 +1,27 @@ +"use server"; + +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { revalidateTag } from "next/cache"; +import { UserDetail, UserResult } from "."; +import { cache } from "react"; + +export interface UserInputs { + username: string; + firstname: string; + lastname: string; +} + + +export const fetchUserDetails = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/user/${id}`, { + next: { tags: ["user"] }, + }); + }); + +export const deleteUser = async (id: number) => { + return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }); + }; \ No newline at end of file diff --git a/src/app/api/user/index.ts b/src/app/api/user/index.ts new file mode 100644 index 0000000..9a6065b --- /dev/null +++ b/src/app/api/user/index.ts @@ -0,0 +1,43 @@ +import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { BASE_API_URL } from "@/config/api"; +import { cache } from "react"; +import "server-only"; + + +export interface UserResult { + action: any; + id: number; + name: string; + locale: string; + username: string; + fullName: string; + firstname: string; + lastname: string; + title: string; + department: string; + email: string; + phone1: string; + phone2: string; + remarks: string; + } + +// export interface DetailedUser extends UserResult { +// username: string; +// password: string +// } + +export interface UserDetail { + authIds: number[]; + data: UserResult; + groupIds: number[]; + } + + export const preloadUser = () => { + fetchUser(); + }; + + export const fetchUser = cache(async () => { + return serverFetchJson(`${BASE_API_URL}/user`, { + next: { tags: ["user"] }, + }); + }); \ No newline at end of file diff --git a/src/components/CreateSkill/CreateSkill.tsx b/src/components/CreateSkill/CreateSkill.tsx new file mode 100644 index 0000000..d264b34 --- /dev/null +++ b/src/components/CreateSkill/CreateSkill.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { + FieldErrors, + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, +} from "react-hook-form"; +import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material"; +import { Check, Close, RestartAlt } from "@mui/icons-material"; +import { useCallback, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import { CreateSkillInputs, saveSkill } from "@/app/api/skill/actions"; +import { Error } from "@mui/icons-material"; +import SkillInfo from "./SkillInfo"; + +interface Props {} + +const CreateSkill: React.FC = () => { + const formProps = useForm(); + const [serverError, setServerError] = useState(""); + const router = useRouter(); + const { t } = useTranslation(); + const [tabIndex, setTabIndex] = useState(0); + const errors = formProps.formState.errors; + + const onSubmit = useCallback>( + async (data) => { + try { + console.log(data); + await saveSkill(data) + router.replace(`/settings/skill`) + } catch (e) { + console.log(e); + setServerError(t("An error has occurred. Please try again later.")); + } + }, + [router] + ); + + const handleCancel = () => { + router.back(); + }; + +// const handleReset = useCallback(() => { +// console.log(defaultValues) +// }, [defaultValues]) + + const handleTabChange = useCallback>( + (_e, newValue) => { + setTabIndex(newValue); + }, + [] + ); + + const hasErrorsInTab = ( + tabIndex: number, + errors: FieldErrors + ) => { + switch (tabIndex) { + case 0: + return Object.keys(errors).length > 0; + default: + false; + } + }; + return ( + <> + + + + + ) : undefined + } + iconPosition="end" + /> + {/* */} + + {serverError && ( + + {serverError} + + )} + {tabIndex === 0 && } + + + + + + + + ); +}; + +export default CreateSkill; diff --git a/src/components/CreateSkill/CreateSkillLoading.tsx b/src/components/CreateSkill/CreateSkillLoading.tsx new file mode 100644 index 0000000..f7d17bf --- /dev/null +++ b/src/components/CreateSkill/CreateSkillLoading.tsx @@ -0,0 +1,40 @@ +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 CreateSkillLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + CreateSkill + + + + + + + + + + + ); +}; + +export default CreateSkillLoading; diff --git a/src/components/CreateSkill/CreateSkillWrapper.tsx b/src/components/CreateSkill/CreateSkillWrapper.tsx new file mode 100644 index 0000000..f2f667a --- /dev/null +++ b/src/components/CreateSkill/CreateSkillWrapper.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import CreateSkill from "./CreateSkill"; +import CreateSkillLoading from "./CreateSkillLoading"; +import { fetchStaff, fetchTeamLeads } from "@/app/api/staff"; +import { useSearchParams } from "next/navigation"; + +interface SubComponents { + Loading: typeof CreateSkillLoading; +} + +const CreateSkillWrapper: React.FC & SubComponents = async () => { + + + return ; +}; + +CreateSkillWrapper.Loading = CreateSkillLoading; + +export default CreateSkillWrapper; diff --git a/src/components/CreateSkill/SkillInfo.tsx b/src/components/CreateSkill/SkillInfo.tsx new file mode 100644 index 0000000..be9724d --- /dev/null +++ b/src/components/CreateSkill/SkillInfo.tsx @@ -0,0 +1,90 @@ +"use client"; +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Grid from "@mui/material/Grid"; +import TextField from "@mui/material/TextField"; +import Typography from "@mui/material/Typography"; +import { useTranslation } from "react-i18next"; +import CardActions from "@mui/material/CardActions"; +import RestartAlt from "@mui/icons-material/RestartAlt"; +import Button from "@mui/material/Button"; +import { Controller, useFormContext } from "react-hook-form"; +import { FormControl, InputLabel, MenuItem, Select } from "@mui/material"; +import { useCallback } from "react"; +import { CreateSkillInputs } from "@/app/api/skill/actions"; + +const SkillInfo: React.FC = ( +) => { + const { t } = useTranslation(); + const { + register, + formState: { errors, defaultValues }, + control, + reset, + resetField, + setValue, + } = useFormContext(); + + const resetSkill = useCallback(() => { + console.log(defaultValues); + if (defaultValues !== undefined) { + resetField("name"); + } + }, [defaultValues]); + + return ( + <> + + + + + {t("Skill Info")} + + + + + + + + + + + + + + + + + ); +}; +export default SkillInfo; diff --git a/src/components/CreateSkill/index.ts b/src/components/CreateSkill/index.ts new file mode 100644 index 0000000..044c4cf --- /dev/null +++ b/src/components/CreateSkill/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateSkillWrapper"; diff --git a/src/components/CreateTeam/CreateTeam.tsx b/src/components/CreateTeam/CreateTeam.tsx index 93b585e..64159c0 100644 --- a/src/components/CreateTeam/CreateTeam.tsx +++ b/src/components/CreateTeam/CreateTeam.tsx @@ -89,7 +89,7 @@ const hasErrorsInTab = ( } iconPosition="end" /> - + {serverError && ( diff --git a/src/components/CreateTeam/StaffAllocation.tsx b/src/components/CreateTeam/StaffAllocation.tsx index c51b839..bbd768c 100644 --- a/src/components/CreateTeam/StaffAllocation.tsx +++ b/src/components/CreateTeam/StaffAllocation.tsx @@ -18,9 +18,21 @@ import { StaffResult } from "@/app/api/staff"; import SearchResults, { Column } from "../SearchResults"; import { Clear, PersonAdd, PersonRemove, Search } from "@mui/icons-material"; import { Card } from "reactstrap"; -import { Box, CardContent, Grid, IconButton, InputAdornment, Stack, Tab, Tabs, TabsProps, TextField, Typography } from "@mui/material"; +import { + Box, + CardContent, + Grid, + IconButton, + InputAdornment, + Stack, + Tab, + Tabs, + TabsProps, + TextField, + Typography, +} from "@mui/material"; import { differenceBy } from "lodash"; -import StarsIcon from '@mui/icons-material/Stars'; +import StarsIcon from "@mui/icons-material/Stars"; export interface Props { allStaffs: StaffResult[]; @@ -35,16 +47,15 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { reset, resetField, } = useFormContext(); - + const initialStaffs = staff.map((s) => ({ ...s })); -// console.log(initialStaffs) + // console.log(initialStaffs) const [filteredStaff, setFilteredStaff] = useState(initialStaffs); const [selectedStaff, setSelectedStaff] = useState( initialStaffs.filter((s) => getValues("addStaffIds")?.includes(s.id)) ); - const [seletedTeamLead, setSeletedTeamLead] = useState() - // Adding / Removing staff + // Adding / Removing staff const addStaff = useCallback((staff: StaffResult) => { setSelectedStaff((s) => [...s, staff]); }, []); @@ -53,27 +64,31 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { setSelectedStaff((s) => s.filter((s) => s.id !== staff.id)); }, []); - const setTeamLead = useCallback((staff: StaffResult) => { - - setSeletedTeamLead(staff.id) - const rearrangedList = getValues("addStaffIds").reduce((acc, num, index) => { - if (num === staff.id && index !== 0) { + const setTeamLead = useCallback( + (staff: StaffResult) => { + const rearrangedList = getValues("addStaffIds").reduce( + (acc, num, index) => { + if (num === staff.id && index !== 0) { acc.splice(index, 1); - acc.unshift(num) - } - return acc; - }, getValues("addStaffIds")); - console.log(rearrangedList) - console.log(selectedStaff) - - const rearrangedStaff = rearrangedList.map((id) => { + acc.unshift(num); + } + return acc; + }, + getValues("addStaffIds") + ); + console.log(rearrangedList); + console.log(selectedStaff); + + const rearrangedStaff = rearrangedList.map((id) => { return selectedStaff.find((staff) => staff.id === id); }); - console.log(rearrangedStaff) - setSelectedStaff(rearrangedStaff as StaffResult[]); + console.log(rearrangedStaff); + setSelectedStaff(rearrangedStaff as StaffResult[]); - setValue("addStaffIds", rearrangedList) - }, [addStaff, selectedStaff]); + setValue("addStaffIds", rearrangedList); + }, + [addStaff, selectedStaff] + ); const clearSubsidiary = useCallback(() => { if (defaultValues !== undefined) { @@ -86,7 +101,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { // Sync with form useEffect(() => { - console.log(selectedStaff) + console.log(selectedStaff); setValue( "addStaffIds", selectedStaff.map((s) => s.id) @@ -94,7 +109,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, [selectedStaff, setValue]); useEffect(() => { - console.log(selectedStaff) + console.log(selectedStaff); }, [selectedStaff]); const StaffPoolColumns = useMemo[]>( @@ -107,7 +122,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, { label: t("Staff Id"), name: "staffId" }, { label: t("Staff Name"), name: "name" }, - { label: t("Current Position"), name: "currentPosition" }, + { label: t("Position"), name: "currentPosition" }, ], [addStaff, t] ); @@ -122,7 +137,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, { label: t("Staff Id"), name: "staffId" }, { label: t("Staff Name"), name: "name" }, - { label: t("Current Position"), name: "currentPosition" }, + { label: t("Position"), name: "currentPosition" }, { label: t("Team Lead"), name: "action", @@ -144,16 +159,16 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { }, []); React.useEffect(() => { - // setFilteredStaff( - // initialStaffs.filter((s) => { - // const q = query.toLowerCase(); - // // s.staffId.toLowerCase().includes(q) - // // const q = query.toLowerCase(); - // // return s.name.toLowerCase().includes(q); - // // s.code.toString().includes(q) || - // // (s.brNo != null && s.brNo.toLowerCase().includes(q)) - // }) - // ); + setFilteredStaff( + initialStaffs.filter((i) => { + const q = query.toLowerCase(); + return ( + i.staffId.toLowerCase().includes(q) || + i.name.toLowerCase().includes(q) || + i.currentPosition.toLowerCase().includes(q) + ); + }) + ); }, [staff, query]); const resetStaff = React.useCallback(() => { @@ -161,8 +176,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { clearSubsidiary(); }, [clearQueryInput, clearSubsidiary]); - const formProps = useForm({ - }); + const formProps = useForm({}); // Tab related const [tabIndex, setTabIndex] = React.useState(0); @@ -170,7 +184,7 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { (_e, newValue) => { setTabIndex(newValue); }, - [], + [] ); return ( @@ -185,48 +199,48 @@ const StaffAllocation: React.FC = ({ allStaffs: staff }) => { {t("staff")} - - - - - - - - ), - }} - /> + + + + + + + + ), + }} + /> + - - - - - - - {tabIndex === 0 && ( - - )} - {tabIndex === 1 && ( - + + - )} - + + + {tabIndex === 0 && ( + + )} + {tabIndex === 1 && ( + + )} + diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index ad68823..9016052 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -31,6 +31,8 @@ import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; import GroupIcon from '@mui/icons-material/Group'; import BusinessIcon from '@mui/icons-material/Business'; +import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; interface NavigationItem { icon: React.ReactNode; @@ -117,10 +119,12 @@ const navigationItems: NavigationItem[] = [ { icon: , label: "Subsidiary", path: "/settings/subsidiary" }, { icon: , label: "Staff", path: "/settings/staff" }, { icon: , label: "Company", path: "/settings/company" }, + { icon: , label: "Skill", path: "/settings/skill" }, { icon: , label: "Department", path: "/settings/department" }, { icon: , label: "Position", path: "/settings/position" }, { icon: , label: "Salary", path: "/settings/salary" }, { icon: , label: "Team", path: "/settings/team" }, + { icon: , label: "User", path: "/settings/user" }, ], }, ]; diff --git a/src/components/SkillSearch/SkillSearch.tsx b/src/components/SkillSearch/SkillSearch.tsx new file mode 100644 index 0000000..01db336 --- /dev/null +++ b/src/components/SkillSearch/SkillSearch.tsx @@ -0,0 +1,96 @@ +"use client"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import SearchBox, { Criterion } from "../SearchBox/index"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import EditNote from "@mui/icons-material/EditNote"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { SkillResult } from "@/app/api/skill"; + +interface Props { + skill: SkillResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const SkillSearch: React.FC = ({ skill }) => { + const { t } = useTranslation(); + const [filteredStaff, setFilteredStaff] = useState(skill); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Staff Name"), + paramName: "name", + type: "text", + }, + ], + [t] + ); + + const onSkillClick = useCallback( + (skill: SkillResult) => { + console.log(skill); + const id = skill.id; + // router.push(`/settings/skill/edit?id=${id}`); + }, + [router, t] + ); + + const deleteClick = useCallback((skill: SkillResult) => { + // deleteDialog(async () => { + // await deleteStaff(skill.id); + // successDialog("Delete Success", t); + // setFilteredStaff((prev) => prev.filter((obj) => obj.id !== skill.id)); + // }, t); + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Actions"), + onClick: onSkillClick, + buttonIcon: , + }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("Description") }, + { + name: "action", + label: t("Actions"), + onClick: deleteClick, + buttonIcon: , + color: "error", + }, + ], + [t, onSkillClick, deleteClick] + ); + + return ( + <> + { + // setFilteredStaff( + // skill.filter( + // (s) => + // s.skillId.toLowerCase().includes(query.skillId.toLowerCase()) && + // s.name.toLowerCase().includes(query.name.toLowerCase()) + // // (query.team === "All" || s.team === query.team) && + // // (query.category === "All" || s.category === query.category) && + // // (query.team === "All" || s.team === query.team), + // ) + // ); + }} + /> + items={filteredStaff} columns={columns} /> + + ); +}; + +export default SkillSearch; diff --git a/src/components/SkillSearch/SkillSearchLoading.tsx b/src/components/SkillSearch/SkillSearchLoading.tsx new file mode 100644 index 0000000..a5959e9 --- /dev/null +++ b/src/components/SkillSearch/SkillSearchLoading.tsx @@ -0,0 +1,40 @@ +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 SkillSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default SkillSearchLoading; diff --git a/src/components/SkillSearch/SkillSearchWrapper.tsx b/src/components/SkillSearch/SkillSearchWrapper.tsx new file mode 100644 index 0000000..33d0547 --- /dev/null +++ b/src/components/SkillSearch/SkillSearchWrapper.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import SkillSearch from "./SkillSearch"; +import SkillSearchLoading from "./SkillSearchLoading"; +import { comboProp, fetchCompanyCombo } from "@/app/api/companys/actions"; +import { fetchTeamCombo } from "@/app/api/team/actions"; +import { fetchDepartmentCombo } from "@/app/api/departments/actions"; +import { fetchPositionCombo } from "@/app/api/positions/actions"; +import { fetchGradeCombo } from "@/app/api/grades/actions"; +import { fetchSkillCombo } from "@/app/api/skill/actions"; +import { fetchSalaryCombo } from "@/app/api/salarys/actions"; +import { SkillResult, fetchSkill } from "@/app/api/skill"; +// import { preloadStaff } from "@/app/api/staff"; + +interface SubComponents { + Loading: typeof SkillSearchLoading; +} + +const SkillSearchWrapper: React.FC & SubComponents = async () => { + const skill = await fetchSkill() + console.log(skill); + + return ; +}; + +SkillSearchWrapper.Loading = SkillSearchLoading; + +export default SkillSearchWrapper; diff --git a/src/components/SkillSearch/index.ts b/src/components/SkillSearch/index.ts new file mode 100644 index 0000000..5833a58 --- /dev/null +++ b/src/components/SkillSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./SkillSearchWrapper"; diff --git a/src/components/StaffSearch/ConfirmDeleteModal.tsx b/src/components/StaffSearch/ConfirmDeleteModal.tsx deleted file mode 100644 index abeb962..0000000 --- a/src/components/StaffSearch/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client"; -import React, { useCallback, useMemo, useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal, Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { Add } from "@mui/icons-material"; -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; -import { ThemeProvider } from "@emotion/react"; - -interface Props { - isOpen: boolean; - onConfirm: (data: any) => void; - onCancel: (data: any | null) => void; - // staff: StaffResult[]; -} - -const ConfirmModal: React.FC = ({ ...props }) => { - const { t } = useTranslation(); - return ( - <> - - - <> - - {t("Confirm")} - - <> - - {t("Are You Sure")} - - - {/* */} - - - - - {/* */} - - - - - ); -}; - -export default ConfirmModal; diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx index e65cfe7..fc6204d 100644 --- a/src/components/StaffSearch/StaffSearch.tsx +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -5,15 +5,11 @@ import SearchBox, { Criterion } from "../SearchBox/index"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; -import DeleteIcon from '@mui/icons-material/Delete'; -import ConfirmModal from "./ConfirmDeleteModal"; +import DeleteIcon from "@mui/icons-material/Delete"; import { deleteStaff } from "@/app/api/staff/actions"; import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; -interface combo { - id: any; - label: string; -} interface Props { staff: StaffResult[]; } @@ -24,8 +20,6 @@ type SearchParamNames = keyof SearchQuery; const StaffSearch: React.FC = ({ staff }) => { const { t } = useTranslation(); const [filteredStaff, setFilteredStaff] = useState(staff); - const [data, setData] = useState(); - const [isOpen, setIsOpen] = useState(false); const router = useRouter(); const searchCriteria: Criterion[] = useMemo( @@ -41,10 +35,10 @@ const StaffSearch: React.FC = ({ staff }) => { paramName: "name", type: "text", }, - { - label: t("Staff ID"), - paramName: "staffId", - type: "text" + { + label: t("Staff ID"), + paramName: "staffId", + type: "text", }, { label: t("Grade"), @@ -59,39 +53,26 @@ const StaffSearch: React.FC = ({ staff }) => { options: ["pos1", "CEO"], }, ], - [t], + [t] ); - const onStaffClick = useCallback((staff: StaffResult) => { - console.log(staff); - const id = staff.id - router.push(`/settings/staff/edit?id=${id}`); - }, [router, t]); - - const deleteClick = (staff: StaffResult) => { - console.log(staff); - setData(staff) - setIsOpen(!isOpen) - }; - - const onConfirm = useCallback(async (staff: StaffResult) => { - console.log(staff); - if (data) - await deleteStaff(data) - setIsOpen(false) - window.location.reload; - }, [deleteStaff, data]); + const onStaffClick = useCallback( + (staff: StaffResult) => { + console.log(staff); + const id = staff.id; + router.push(`/settings/staff/edit?id=${id}`); + }, + [router, t] + ); - const onCancel = useCallback((staff: StaffResult) => { - console.log(staff); - setIsOpen(false) + const deleteClick = useCallback((staff: StaffResult) => { + deleteDialog(async () => { + await deleteStaff(staff.id); + successDialog("Delete Success", t); + setFilteredStaff((prev) => prev.filter((obj) => obj.id !== staff.id)); + }, t); }, []); - // useEffect(() => { - // console.log("id"); - // console.log(id); - // }, [id]); - const columns = useMemo[]>( () => [ { @@ -110,34 +91,30 @@ const StaffSearch: React.FC = ({ staff }) => { label: t("Actions"), onClick: deleteClick, buttonIcon: , + color: "error", }, ], - [t, onStaffClick, deleteClick], + [t, onStaffClick, deleteClick] ); return ( <> { + onSearch={(query) => { setFilteredStaff( staff.filter( - (s) => - s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && - s.name.toLowerCase().includes(query.name.toLowerCase()) + (s) => + s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && + s.name.toLowerCase().includes(query.name.toLowerCase()) // (query.team === "All" || s.team === query.team) && // (query.category === "All" || s.category === query.category) && // (query.team === "All" || s.team === query.team), ) - ) + ); }} /> items={filteredStaff} columns={columns} /> - ); }; diff --git a/src/components/TeamSearch/ConfirmDeleteModal.tsx b/src/components/TeamSearch/ConfirmDeleteModal.tsx deleted file mode 100644 index a5e7ed0..0000000 --- a/src/components/TeamSearch/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; -import React, { useCallback, useMemo, useState } from "react"; -import Button from "@mui/material/Button"; -import { Card, Modal, Stack, Typography } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { Add } from "@mui/icons-material"; -import Check from "@mui/icons-material/Check"; -import Close from "@mui/icons-material/Close"; -import { TSMS_BUTTON_THEME } from "@/theme/colorConst"; -import { ThemeProvider } from "@emotion/react"; - -interface Props { - isOpen: boolean; - onConfirm: (data: any) => void; - onCancel: (data: any | null) => void; -} - -const ConfirmModal: React.FC = ({ ...props }) => { - const { t } = useTranslation(); - return ( - <> - - - <> - - {t("Confirm")} - - <> - - {t("Are You Sure")} - - - {/* */} - - - - - {/* */} - - - - - ); -}; - -export default ConfirmModal; diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx index b2cc9e8..a1db872 100644 --- a/src/components/TeamSearch/TeamSearch.tsx +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -6,12 +6,10 @@ import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults/index"; import EditNote from "@mui/icons-material/EditNote"; -import DeleteIcon from '@mui/icons-material/Delete'; -import { deleteStaff } from "@/app/api/staff/actions"; +import DeleteIcon from "@mui/icons-material/Delete"; import { useRouter } from "next/navigation"; -import ConfirmModal from "./ConfirmDeleteModal"; import { deleteTeam } from "@/app/api/team/actions"; - +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; interface Props { team: TeamResult[]; @@ -20,109 +18,90 @@ type SearchQuery = Partial>; type SearchParamNames = keyof SearchQuery; const TeamSearch: React.FC = ({ team }) => { - const { t } = useTranslation(); - const [filteredTeam, setFilteredTeam] = useState(team); - const [data, setData] = useState(); - const [isOpen, setIsOpen] = useState(false); - const router = useRouter(); - - const searchCriteria: Criterion[] = useMemo( - () => [ - { - label: t("Team Name"), - paramName: "name", - type: "text", - }, - { - label: t("Team Code"), - paramName: "code", - type: "text", - }, - { - label: t("Team Description"), - paramName: "description", - type: "text", - }, - ], - [t], - ); + const { t } = useTranslation(); + const [filteredTeam, setFilteredTeam] = useState(team); + const router = useRouter(); - const onTeamClick = useCallback((team: TeamResult) => { - console.log(team); - const id = team.id - router.push(`/settings/team/edit?id=${id}`); - }, [router, t]); - - // const onDeleteClick = useCallback((team: TeamResult) => { - // console.log(team); - // deleteTeam + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("Team Name"), + paramName: "name", + type: "text", + }, + { + label: t("Team Code"), + paramName: "code", + type: "text", + }, + { + label: t("Team Description"), + paramName: "description", + type: "text", + }, + ], + [t] + ); - // }, [router, t]); + const onTeamClick = useCallback( + (team: TeamResult) => { + console.log(team); + const id = team.id; + router.push(`/settings/team/edit?id=${id}`); + }, + [router, t] + ); - const onDeleteClick = (team: TeamResult) => { - console.log(team); - setData(team) - setIsOpen(!isOpen) - }; + const onDeleteClick = useCallback((team: TeamResult) => { + deleteDialog(async () => { + await deleteTeam(team.id); - const onConfirm = useCallback(async (team: TeamResult) => { - console.log(team); - if (data) - await deleteTeam(data) - setIsOpen(false) - window.location.reload; - }, [deleteTeam, data]); + successDialog("Delete Success", t); - const onCancel = useCallback(() => { - setIsOpen(false) - }, []); + setFilteredTeam((prev) => prev.filter((obj) => obj.id !== team.id)); + }, t); + }, []); - const columns = useMemo[]>( - () => [ - { - name: "action", - label: t("Edit"), - onClick: onTeamClick, - buttonIcon: , - }, - { name: "name", label: t("Name") }, - { name: "code", label: t("Code") }, - { name: "description", label: t("description") }, - { - name: "action", - label: t("Delete"), - onClick: onDeleteClick, - buttonIcon: , - }, - ], - [t], - ); + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Edit"), + onClick: onTeamClick, + buttonIcon: , + }, + { name: "name", label: t("Name") }, + { name: "code", label: t("Code") }, + { name: "description", label: t("description") }, + { name: "staffName", label: t("TeamLead") }, + { + name: "action", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, + ], + [t] + ); return ( - <> - + { - // setFilteredStaff( - // staff.filter( - // (s) => - // s.staffId.toLowerCase().includes(query.staffId.toLowerCase()) && - // s.name.toLowerCase().includes(query.name.toLowerCase()) - // // (query.team === "All" || s.team === query.team) && - // // (query.category === "All" || s.category === query.category) && - // // (query.team === "All" || s.team === query.team), - // ) - // ) + onSearch={(query) => { + setFilteredTeam( + team.filter( + (t) => + t.name.toLowerCase().includes(query.name.toLowerCase()) && + t.code.toLowerCase().includes(query.code.toLowerCase()) && + t.description.toLowerCase().includes(query.description.toLowerCase()) + ) + ) }} /> items={filteredTeam} columns={columns} /> - - - + ); }; export default TeamSearch; diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx new file mode 100644 index 0000000..095c544 --- /dev/null +++ b/src/components/UserSearch/UserSearch.tsx @@ -0,0 +1,98 @@ +"use client"; + +import SearchBox, { Criterion } from "../SearchBox"; +import { useCallback, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults/index"; +import EditNote from "@mui/icons-material/EditNote"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useRouter } from "next/navigation"; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { UserResult } from "@/app/api/user"; +import { deleteUser } from "@/app/api/user/actions"; + +interface Props { + users: UserResult[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const UserSearch: React.FC = ({ users }) => { + const { t } = useTranslation(); + const [filteredUser, setFilteredUser] = useState(users); + const router = useRouter(); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { + label: t("User Name"), + paramName: "title", + type: "text", + }, + ], + [t] + ); + + const onUserClick = useCallback( + (users: UserResult) => { + console.log(users); + router.push(`/settings/user/edit?id=${users.id}`) + }, + [router, t] + ); + + const onDeleteClick = useCallback((users: UserResult) => { + deleteDialog(async () => { + await deleteUser(users.id); + + successDialog("Delete Success", t); + + setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id)); + }, t); + }, []); + + const columns = useMemo[]>( + () => [ + { + name: "action", + label: t("Edit"), + onClick: onUserClick, + buttonIcon: , + }, + { name: "name", label: t("UserName") }, + { name: "fullName", label: t("FullName") }, + { name: "title", label: t("Title") }, + { name: "department", label: t("Department") }, + { name: "email", label: t("Email") }, + { name: "phone1", label: t("Phone") }, + { + name: "action", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, + ], + [t] + ); + + return ( + <> + { + // setFilteredUser( + // users.filter( + // (t) => + // t.name.toLowerCase().includes(query.name.toLowerCase()) && + // t.code.toLowerCase().includes(query.code.toLowerCase()) && + // t.description.toLowerCase().includes(query.description.toLowerCase()) + // ) + // ) + }} + /> + items={filteredUser} columns={columns} /> + + ); +}; +export default UserSearch; diff --git a/src/components/UserSearch/UserSearchLoading.tsx b/src/components/UserSearch/UserSearchLoading.tsx new file mode 100644 index 0000000..535a751 --- /dev/null +++ b/src/components/UserSearch/UserSearchLoading.tsx @@ -0,0 +1,40 @@ +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 UserSearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default UserSearchLoading; diff --git a/src/components/UserSearch/UserSearchWrapper.tsx b/src/components/UserSearch/UserSearchWrapper.tsx new file mode 100644 index 0000000..beaef92 --- /dev/null +++ b/src/components/UserSearch/UserSearchWrapper.tsx @@ -0,0 +1,19 @@ +import React from "react"; +import UserSearch from "./UserSearch"; +import UserSearchLoading from "./UserSearchLoading"; +import { UserResult, fetchUser } from "@/app/api/user"; + +interface SubComponents { + Loading: typeof UserSearchLoading; +} + +const UserSearchWrapper: React.FC & SubComponents = async () => { +const users = await fetchUser() + console.log(users); + + return ; +}; + +UserSearchWrapper.Loading = UserSearchLoading; + +export default UserSearchWrapper; diff --git a/src/components/UserSearch/index.ts b/src/components/UserSearch/index.ts new file mode 100644 index 0000000..c2e98d7 --- /dev/null +++ b/src/components/UserSearch/index.ts @@ -0,0 +1 @@ +export { default } from "./UserSearchWrapper"; From dedeff4b413fd2c2e2aa6f3f52b1e48fd76ad647 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Apr 2024 16:41:03 +0800 Subject: [PATCH 02/11] update claim, add EX02 report --- .../EX02ProjectCashFlowReport/page.tsx | 25 +++++++++ src/app/api/claims/actions.ts | 2 +- src/app/api/reports/actions.ts | 18 +++++++ src/app/api/reports/index.ts | 8 +++ src/app/utils/commonUtil.ts | 16 ++++++ src/app/utils/fetchUtil.ts | 26 ++++++++-- src/components/Breadcrumb/Breadcrumb.tsx | 1 + src/components/ClaimDetail/ClaimDetail.tsx | 3 +- .../ClaimDetail/ClaimFormInputGrid.tsx | 9 ++-- src/components/ClaimSearch/ClaimSearch.tsx | 12 ++--- .../GenerateEX02ProjectCashFlowReport.tsx | 52 +++++++++++++++++++ ...nerateEX02ProjectCashFlowReportLoading.tsx | 38 ++++++++++++++ ...nerateEX02ProjectCashFlowReportWrapper.tsx | 18 +++++++ .../index.ts | 1 + .../NavigationContent/NavigationContent.tsx | 1 + src/i18n/en/claim.json | 1 + src/i18n/en/report.json | 3 ++ src/i18n/zh/claim.json | 1 + src/i18n/zh/report.json | 3 ++ 19 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx create mode 100644 src/app/api/reports/actions.ts create mode 100644 src/app/api/reports/index.ts create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx create mode 100644 src/components/GenerateEX02ProjectCashFlowReport/index.ts create mode 100644 src/i18n/en/report.json create mode 100644 src/i18n/zh/report.json diff --git a/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx new file mode 100644 index 0000000..d1d54d8 --- /dev/null +++ b/src/app/(main)/analytics/EX02ProjectCashFlowReport/page.tsx @@ -0,0 +1,25 @@ +import { Metadata } from "next"; +import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateEX02ProjectCashFlowReport from "@/components/GenerateEX02ProjectCashFlowReport"; + +export const metadata: Metadata = { + title: "EX02 - Project Cash Flow Report", +}; + +const ProjectCashFlowReport: React.FC = async () => { + fetchProjects(); + + return ( + <> + + }> + + + + + ); +}; + +export default ProjectCashFlowReport; diff --git a/src/app/api/claims/actions.ts b/src/app/api/claims/actions.ts index d607c48..542effe 100644 --- a/src/app/api/claims/actions.ts +++ b/src/app/api/claims/actions.ts @@ -21,7 +21,7 @@ export interface ClaimDetailTable { id: number; invoiceDate: Date; description: string; - project: ProjectCombo; + project: number; amount: number; supportingDocumentName: string; oldSupportingDocument: SupportingDocument; diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts new file mode 100644 index 0000000..e0db4c4 --- /dev/null +++ b/src/app/api/reports/actions.ts @@ -0,0 +1,18 @@ +"use server"; + +import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; +import { EX02ProjectCashFlowReportRequest } from "."; +import { BASE_API_URL } from "@/config/api"; + +export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowReportRequest) => { + const reportBlob = await serverFetchBlob( + `${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + + return reportBlob +}; \ No newline at end of file diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts new file mode 100644 index 0000000..6baa7aa --- /dev/null +++ b/src/app/api/reports/index.ts @@ -0,0 +1,8 @@ +// EX02 - Project Cash Flow Report +export interface EX02ProjectCashFlowReportFilter { + project: string[]; +} + +export interface EX02ProjectCashFlowReportRequest { + projectId: number; +} \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index d4c71b6..f0cc208 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -20,4 +20,20 @@ export const dateInRange = (currentDate: string, startDate: string, endDate: str return true } } +} + +function s2ab(s: string) { + var buf = new ArrayBuffer(s.length); + var view = new Uint8Array(buf); + for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; + return buf; +} + +export const downloadFile = (blob: Blob | string, type: string, filename: string) => { + + const url = URL.createObjectURL(typeof blob === "string" ? new Blob([blob], { type: type }) : blob); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", filename); + link.click(); } \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 0aaa798..fa11529 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -14,9 +14,9 @@ export const serverFetch: typeof fetch = async (input, init) => { ...init?.headers, ...(accessToken ? { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json" - } + Authorization: `Bearer ${accessToken}`, + Accept: "application/json" + } : {}), }, }); @@ -56,6 +56,26 @@ export async function serverFetchWithNoContent(...args: FetchParams) { } } +export async function serverFetchBlob(...args: FetchParams) { + const response = await serverFetch(...args); + + if (response.ok) { + console.log(response) + const blob = await response.blob() + const blobText = await blob.text(); + const blobType = await blob.type; + return {filename: response.headers.get("filename"), blobText: blobText, blobType: blobType}; + } else { + switch (response.status) { + case 401: + signOutUser(); + default: + console.error(await response.text()); + throw Error("Something went wrong fetching data in server."); + } + } +} + export const signOutUser = () => { const headersList = headers(); const referer = headersList.get("referer"); diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 314ea63..3d6123a 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -28,6 +28,7 @@ const pathToLabelMap: { [path: string]: string } = { "/settings/position": "Position", "/settings/position/new": "Create Position", "/settings/salarys": "Salary", + "/analytics/EX02ProjectCashFlowReport": "EX02 - Project Cash Flow Report", }; const Breadcrumb = () => { diff --git a/src/components/ClaimDetail/ClaimDetail.tsx b/src/components/ClaimDetail/ClaimDetail.tsx index db74447..54a82a5 100644 --- a/src/components/ClaimDetail/ClaimDetail.tsx +++ b/src/components/ClaimDetail/ClaimDetail.tsx @@ -75,9 +75,10 @@ const ClaimDetail: React.FC = ({ projectCombo }) => { const formData = new FormData() formData.append("expenseType", data.expenseType) data.addClaimDetails.forEach((claimDetail) => { + console.log(claimDetail) formData.append("addClaimDetailIds", JSON.stringify(claimDetail.id)) formData.append("addClaimDetailInvoiceDates", convertDateToString(claimDetail.invoiceDate, "YYYY-MM-DD")) - formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project.id)) + formData.append("addClaimDetailProjectIds", JSON.stringify(claimDetail.project)) formData.append("addClaimDetailDescriptions", claimDetail.description) formData.append("addClaimDetailAmounts", JSON.stringify(claimDetail.amount)) formData.append("addClaimDetailNewSupportingDocuments", claimDetail.newSupportingDocument) diff --git a/src/components/ClaimDetail/ClaimFormInputGrid.tsx b/src/components/ClaimDetail/ClaimFormInputGrid.tsx index 24807bd..6aac620 100644 --- a/src/components/ClaimDetail/ClaimFormInputGrid.tsx +++ b/src/components/ClaimDetail/ClaimFormInputGrid.tsx @@ -371,20 +371,21 @@ const ClaimFormInputGrid: React.FC = ({ flex: 1, editable: true, type: "singleSelect", - getOptionLabel: (value: any) => { + getOptionLabel: (value: ProjectCombo) => { return !value?.code || value?.code.length === 0 ? `${value?.name}` : `${value?.code} - ${value?.name}`; }, - getOptionValue: (value: any) => value, + getOptionValue: (value: ProjectCombo) => value.id, valueOptions: () => { const options = projectCombo ?? [] if (options.length === 0) { options.push({ id: -1, code: "", name: "No Projects" }) } - return options; + + return options as ProjectCombo[]; }, valueGetter: (params) => { - return params.value ?? projectCombo[0].id ?? -1 + return params.value ?? projectCombo[0] ?? { id: -1, code: "", name: "No Projects" } as ProjectCombo }, }, { diff --git a/src/components/ClaimSearch/ClaimSearch.tsx b/src/components/ClaimSearch/ClaimSearch.tsx index c0ab01f..304993a 100644 --- a/src/components/ClaimSearch/ClaimSearch.tsx +++ b/src/components/ClaimSearch/ClaimSearch.tsx @@ -50,12 +50,12 @@ const ClaimSearch: React.FC = ({ claims }) => { const columns = useMemo[]>( () => [ - // { - // name: "action", - // label: t("Actions"), - // onClick: onClaimClick, - // buttonIcon: , - // }, + { + name: "id", + label: t("Details"), + onClick: onClaimClick, + buttonIcon: , + }, { name: "created", label: t("Creation Date"), type: "date" }, { name: "code", label: t("Claim Code") }, // { name: "project", label: t("Related Project Name") }, diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx new file mode 100644 index 0000000..d0bc75f --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx @@ -0,0 +1,52 @@ +"use client"; + +import React, { useMemo } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import { ProjectResult } from "@/app/api/projects"; +import { EX02ProjectCashFlowReportFilter } from "@/app/api/reports"; +import { fetchEX02ProjectCashFlowReport } from "@/app/api/reports/actions"; +import { downloadFile } from "@/app/utils/commonUtil"; + +interface Props { + projects: ProjectResult[]; +} + +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + +const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { + const { t } = useTranslation(); + const projectCombo = projects.map(project => `${project.code} - ${project.name}`) + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: t("Project"), paramName: "project", type: "select", options: projectCombo}, + ], + [t], + ); + + return ( + <> + { + const projectIndex = projectCombo.findIndex(project => project === query.project) + const response = await fetchEX02ProjectCashFlowReport({projectId: projects[projectIndex].id}) + console.log(response) + if (response) { + downloadFile(response.blobText, response.blobType, response.filename!!) + } + + // const url = URL.createObjectURL(response.blob); + // const link = document.createElement("a"); + // link.href = url; + // link.setAttribute("download", "abc.xlsx"); + // link.click(); + }} + /> + + ); +}; + +export default GenerateEX02ProjectCashFlowReport; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx new file mode 100644 index 0000000..1792221 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportLoading.tsx @@ -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 GenerateEX02ProjectCashFlowReportLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + ); +}; + +export default GenerateEX02ProjectCashFlowReportLoading; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx new file mode 100644 index 0000000..5bf1089 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReportWrapper.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import GenerateEX02ProjectCashFlowReportLoading from "./GenerateEX02ProjectCashFlowReportLoading"; +import { fetchProjects } from "@/app/api/projects"; +import GenerateEX02ProjectCashFlowReport from "./GenerateEX02ProjectCashFlowReport"; + +interface SubComponents { + Loading: typeof GenerateEX02ProjectCashFlowReportLoading; +} + +const GenerateEX02ProjectCashFlowReportWrapper: React.FC & SubComponents = async () => { + const projects = await fetchProjects(); + + return ; +}; + +GenerateEX02ProjectCashFlowReportWrapper.Loading = GenerateEX02ProjectCashFlowReportLoading; + +export default GenerateEX02ProjectCashFlowReportWrapper; \ No newline at end of file diff --git a/src/components/GenerateEX02ProjectCashFlowReport/index.ts b/src/components/GenerateEX02ProjectCashFlowReport/index.ts new file mode 100644 index 0000000..b547e33 --- /dev/null +++ b/src/components/GenerateEX02ProjectCashFlowReport/index.ts @@ -0,0 +1 @@ +export { default } from "./GenerateEX02ProjectCashFlowReportWrapper"; \ No newline at end of file diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 9016052..aef7c0d 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -110,6 +110,7 @@ const navigationItems: NavigationItem[] = [ {icon: , label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"}, {icon: , label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, {icon: , label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, + {icon: , label:"EX02 - Project Cash Flow Report", path: "/analytics/EX02ProjectCashFlowReport"}, ], }, { diff --git a/src/i18n/en/claim.json b/src/i18n/en/claim.json index 51f657c..b5b5e42 100644 --- a/src/i18n/en/claim.json +++ b/src/i18n/en/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "Please ensure the projects are selected", "Please ensure the amount are correct": "Please ensure the amount are correct", + "Details": "Details", "Description": "Description", "Actions": "Actions" } \ No newline at end of file diff --git a/src/i18n/en/report.json b/src/i18n/en/report.json new file mode 100644 index 0000000..e7e61fb --- /dev/null +++ b/src/i18n/en/report.json @@ -0,0 +1,3 @@ +{ + "Project": "Project" +} \ No newline at end of file diff --git a/src/i18n/zh/claim.json b/src/i18n/zh/claim.json index 92e5b7e..9e2de7e 100644 --- a/src/i18n/zh/claim.json +++ b/src/i18n/zh/claim.json @@ -31,6 +31,7 @@ "Please ensure the projects are selected": "請確保所有項目欄位已選擇", "Please ensure the amount are correct": "請確保所有金額輸入正確", + "Details": "詳請", "Description": "描述", "Actions": "行動" } \ No newline at end of file diff --git a/src/i18n/zh/report.json b/src/i18n/zh/report.json new file mode 100644 index 0000000..a6257cf --- /dev/null +++ b/src/i18n/zh/report.json @@ -0,0 +1,3 @@ +{ + "Project": "項目" +} \ No newline at end of file From 1f8a4e18c933004d12dd6a62f0e7e2de3d472187 Mon Sep 17 00:00:00 2001 From: "Mac\\David" Date: Mon, 29 Apr 2024 16:48:28 +0800 Subject: [PATCH 03/11] add project resource summary page --- .../dashboard/ProjectResourceSummary/page.tsx | 29 + .../dashboard/StaffUtilization/page.tsx | 2 +- src/app/api/clientprojects/index.ts | 6 +- src/app/api/resourcesummary/index.ts | 53 ++ .../CustomDatagrid/CustomDatagrid.tsx | 18 +- .../NavigationContent/NavigationContent.tsx | 6 + .../ProgressByClient/ProgressByClient.tsx | 3 +- .../ProgressByClientSearch.tsx | 21 +- .../ProgressByTeam/ProgressByTeam.tsx | 3 +- .../ProjectResourceSummary.tsx | 548 ++++++++++++++++++ .../ProjectResourceSummary/index.ts | 1 + .../ProjectResourceSummarySearch.tsx | 75 +++ .../ProjectResourceSummarySearchLoading.tsx | 40 ++ .../ProjectResourceSummarySearchWrapper.tsx | 20 + .../ProjectResourceSummarySearch/index.ts | 1 + 15 files changed, 814 insertions(+), 12 deletions(-) create mode 100644 src/app/(main)/dashboard/ProjectResourceSummary/page.tsx create mode 100644 src/app/api/resourcesummary/index.ts create mode 100644 src/components/ProjectResourceSummary/ProjectResourceSummary.tsx create mode 100644 src/components/ProjectResourceSummary/index.ts create mode 100644 src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx create mode 100644 src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx create mode 100644 src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx create mode 100644 src/components/ProjectResourceSummarySearch/index.ts diff --git a/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx b/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx new file mode 100644 index 0000000..5dc2f77 --- /dev/null +++ b/src/app/(main)/dashboard/ProjectResourceSummary/page.tsx @@ -0,0 +1,29 @@ +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; +import DashboardPage from "@/components/DashboardPage/DashboardPage"; +import DashboardPageButton from "@/components/DashboardPage/DashboardTabButton"; +import { Suspense } from "react"; +import Tabs, { TabsProps } from "@mui/material/Tabs"; +import Tab from "@mui/material/Tab"; +import Typography from "@mui/material/Typography"; +import StaffUtilizationComponent from "@/components/StaffUtilization"; +import ProjectResourceSummarySearch from "@/components/ProjectResourceSummarySearch"; +import { ResourceSummaryResult } from "@/app/api/resourcesummary"; + +export const metadata: Metadata = { + title: "Project Resource Summary", +}; + +const ProjectResourceSummary: React.FC = () => { + return ( + + + Project Resource Summary + + }> + + + + ); +}; +export default ProjectResourceSummary; diff --git a/src/app/(main)/dashboard/StaffUtilization/page.tsx b/src/app/(main)/dashboard/StaffUtilization/page.tsx index 2ddea02..87bb6c0 100644 --- a/src/app/(main)/dashboard/StaffUtilization/page.tsx +++ b/src/app/(main)/dashboard/StaffUtilization/page.tsx @@ -10,7 +10,7 @@ import Typography from "@mui/material/Typography"; import StaffUtilizationComponent from "@/components/StaffUtilization"; export const metadata: Metadata = { - title: "Project Status by Client", + title: "Staff Utilization", }; const StaffUtilization: React.FC = () => { diff --git a/src/app/api/clientprojects/index.ts b/src/app/api/clientprojects/index.ts index 5c65810..3eed422 100644 --- a/src/app/api/clientprojects/index.ts +++ b/src/app/api/clientprojects/index.ts @@ -27,7 +27,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 5, }, { - id: 1, + id: 2, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-001", @@ -35,7 +35,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 5, }, { - id: 1, + id: 3, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-002", @@ -43,7 +43,7 @@ const mockProjects: ClientProjectResult[] = [ NoOfProjects: 3, }, { - id: 1, + id: 4, clientCode: "CUST-001", clientName: "Client A", SubsidiaryClientCode: "SUBS-003", diff --git a/src/app/api/resourcesummary/index.ts b/src/app/api/resourcesummary/index.ts new file mode 100644 index 0000000..ffaba69 --- /dev/null +++ b/src/app/api/resourcesummary/index.ts @@ -0,0 +1,53 @@ +import { cache } from "react"; + +export interface ResourceSummaryResult { + id: number; + projectCode: string; + projectName: string; + clientCode: string; + clientName: string; + clientCodeAndName: string; +} + +export const preloadProjects = () => { + fetchResourceSummary(); +}; + +export const fetchResourceSummary = cache(async () => { + return mockProjects; +}); + +const mockProjects: ResourceSummaryResult[] = [ + { + id: 1, + projectCode: 'C-1001-001', + projectName: 'Consultancy Project A', + clientCode: 'Client-001', + clientName: 'AAA Construction', + clientCodeAndName: 'Client-001 - AAA Construction', + }, + { + id: 2, + projectCode: 'C-1002-001', + projectName: 'Consultancy Project B', + clientCode: 'Client-001', + clientName: 'AAA Construction', + clientCodeAndName: 'Client-001 - AAA Construction', + }, + { + id: 3, + projectCode: 'C-1003-001', + projectName: 'Consultancy Project C', + clientCode: 'Client-002', + clientName: 'BBB Construction', + clientCodeAndName: 'Client-002 - BBB Construction', + }, + { + id: 4, + projectCode: 'C-1004-001', + projectName: 'Consultancy Project D', + clientCode: 'Client-002', + clientName: 'BBB Construction', + clientCodeAndName: 'Client-002 - BBB Construction', + }, +]; diff --git a/src/components/CustomDatagrid/CustomDatagrid.tsx b/src/components/CustomDatagrid/CustomDatagrid.tsx index 314ba6c..4867623 100644 --- a/src/components/CustomDatagrid/CustomDatagrid.tsx +++ b/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 } from "@mui/x-data-grid"; +import { DataGrid, GridColDef, GridRowSelectionModel, GridColumnGroupingModel} from "@mui/x-data-grid"; import { darken, lighten, styled } from "@mui/material/styles"; import { useState } from "react"; @@ -19,6 +19,8 @@ interface CustomDatagridProps { newSelectionModel: GridRowSelectionModel, ) => void; selectionModel?: any; + columnGroupingModel?: any; + pageSize?:any; } const CustomDatagrid: React.FC = ({ @@ -32,6 +34,8 @@ const CustomDatagrid: React.FC = ({ checkboxSelection, // Destructure the new prop onRowSelectionModelChange, // Destructure the new prop selectionModel, + columnGroupingModel, + pageSize, ...props }) => { const modifiedColumns = columns.map((column) => { @@ -193,6 +197,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, }} @@ -222,6 +228,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, }} @@ -251,6 +259,8 @@ const CustomDatagrid: React.FC = ({ editMode="row" checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} style={{ marginRight: 20 }} initialState={{ pagination: { paginationModel: { pageSize: 10 } }, @@ -282,8 +292,10 @@ const CustomDatagrid: React.FC = ({ style={{ marginRight: 0 }} checkboxSelection={checkboxSelection} onRowSelectionModelChange={onRowSelectionModelChange} + experimentalFeatures={{ columnGrouping: true }} + columnGroupingModel={columnGroupingModel} initialState={{ - pagination: { paginationModel: { pageSize: 10 } }, + pagination: { paginationModel: { pageSize: pageSize ?? 10 } }, }} className="customDataGrid" sx={{ @@ -293,7 +305,7 @@ const CustomDatagrid: React.FC = ({ "& .MuiDataGrid-cell:hover": { color: "primary.main", }, - height: 300, + height: dataGridHeight ?? 300, "& .MuiDataGrid-root": { overflow: "auto", }, diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index aef7c0d..1c1f12d 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -31,6 +31,7 @@ import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; import GroupIcon from '@mui/icons-material/Group'; import BusinessIcon from '@mui/icons-material/Business'; +import ViewWeekIcon from '@mui/icons-material/ViewWeek'; import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; @@ -78,6 +79,11 @@ const navigationItems: NavigationItem[] = [ label: "Staff Utilization", path: "/dashboard/StaffUtilization", }, + { + icon: , + label: "Project Resource Summary", + path: "/dashboard/ProjectResourceSummary", + } ], }, { diff --git a/src/components/ProgressByClient/ProgressByClient.tsx b/src/components/ProgressByClient/ProgressByClient.tsx index 6815f73..6750392 100644 --- a/src/components/ProgressByClient/ProgressByClient.tsx +++ b/src/components/ProgressByClient/ProgressByClient.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; import { Card, CardHeader } from "@mui/material"; import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; -import ReactApexChart from "react-apexcharts"; +// import ReactApexChart from "react-apexcharts"; import { ApexOptions } from "apexcharts"; import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import ReportProblemIcon from "@mui/icons-material/ReportProblem"; @@ -18,6 +18,7 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByClientSearch from "@/components/ProgressByClientSearch"; import { Suspense } from "react"; +const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); const ProgressByClient: React.FC = () => { const [activeTab, setActiveTab] = useState("financialSummary"); diff --git a/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx b/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx index 553a306..2062919 100644 --- a/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx +++ b/src/components/ProgressByClientSearch/ProgressByClientSearch.tsx @@ -1,11 +1,13 @@ "use client"; import { ProjectResult } from "@/app/api/projects"; -import React, { useMemo, useState } from "react"; +import React, { useMemo, useState, useCallback } from "react"; import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import { ClientProjectResult } from "@/app/api/clientprojects"; +import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; interface Props { projects: ClientProjectResult[]; @@ -15,7 +17,7 @@ type SearchParamNames = keyof SearchQuery; const ProgressByClientSearch: React.FC = ({ projects }) => { const { t } = useTranslation("projects"); - + const searchParams = useSearchParams() // If project searching is done on the server-side, then no need for this. const [filteredProjects, setFilteredProjects] = useState(projects); @@ -27,15 +29,28 @@ const ProgressByClientSearch: React.FC = ({ projects }) => { [t], ); + const onTaskClick = useCallback((clientProjectResult: ClientProjectResult) => { + const params = new URLSearchParams(searchParams.toString()) + params.set("id", clientProjectResult.id.toString()) + console.log(clientProjectResult) +}, []); + const columns = useMemo[]>( () => [ + { + name: "id", + label: t("Details"), + onClick: onTaskClick, + buttonIcon: , + }, { name: "clientCode", label: t("Client Code") }, { name: "clientName", label: t("Client Name") }, { name: "SubsidiaryClientCode", label: t("Subsidiary Code") }, { name: "SubsidiaryClientName", label: t("Subisdiary") }, { name: "NoOfProjects", label: t("No. of Projects") }, ], - [t], + [onTaskClick, t], + // [t], ); return ( diff --git a/src/components/ProgressByTeam/ProgressByTeam.tsx b/src/components/ProgressByTeam/ProgressByTeam.tsx index bee6f73..164a00d 100644 --- a/src/components/ProgressByTeam/ProgressByTeam.tsx +++ b/src/components/ProgressByTeam/ProgressByTeam.tsx @@ -8,7 +8,7 @@ import { useTranslation } from "react-i18next"; import { Card, CardHeader } from "@mui/material"; import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; -import ReactApexChart from "react-apexcharts"; +// import ReactApexChart from "react-apexcharts"; import { ApexOptions } from "apexcharts"; import { GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import ReportProblemIcon from "@mui/icons-material/ReportProblem"; @@ -18,6 +18,7 @@ import { AnyARecord, AnyCnameRecord } from "dns"; import SearchBox, { Criterion } from "../SearchBox"; import ProgressByTeamSearch from "@/components/ProgressByTeamSearch"; import { Suspense } from "react"; +const ReactApexChart = dynamic(() => import('react-apexcharts'), { ssr: false }); const ProgressByTeam: React.FC = () => { const [activeTab, setActiveTab] = useState("financialSummary"); diff --git a/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx b/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx new file mode 100644 index 0000000..57d25d2 --- /dev/null +++ b/src/components/ProjectResourceSummary/ProjectResourceSummary.tsx @@ -0,0 +1,548 @@ +"use client"; +import * as React from "react"; +import Grid from "@mui/material/Grid"; +import { useState, useEffect, useMemo } from "react"; +import Paper from "@mui/material/Paper"; +import { TFunction } from "i18next"; +import { useTranslation } from "react-i18next"; +import { Card, CardHeader } from "@mui/material"; +import CustomSearchForm from "../CustomSearchForm/CustomSearchForm"; +import CustomDatagrid from "../CustomDatagrid/CustomDatagrid"; +import ReactApexChart from "react-apexcharts"; +import { ApexOptions } from "apexcharts"; +import { DataGrid, GridColDef, GridRowSelectionModel} from "@mui/x-data-grid"; +import ReportProblemIcon from "@mui/icons-material/ReportProblem"; +import dynamic from "next/dynamic"; +import "../../app/global.css"; +import { AnyARecord, AnyCnameRecord } from "dns"; +import SearchBox, { Criterion } from "../SearchBox"; +import ProgressByClientSearch from "@/components/ProgressByClientSearch"; +import { Suspense } from "react"; +import { getPossibleInstrumentationHookFilenames } from "next/dist/build/utils"; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Collapse from '@mui/material/Collapse'; +import IconButton from '@mui/material/IconButton'; +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; + + +const ProjectResourceSummary: React.FC = () => { + const [SearchCriteria, setSearchCriteria] = React.useState({}); + const { t } = useTranslation("dashboard"); + const [selectionModel, setSelectionModel]: any[] = React.useState([]); + const [projectName, setProjectName]:any = React.useState("NA"); + const [projectFee, setProjectFee]:any = React.useState(0); + const [status, setStatus]:any = React.useState("NA"); + const [plannedResources, setPlannedResources]:any = React.useState(0); + const [actualResourcesSpent, setActualResourcesSpent]:any = React.useState(0); + const [remainingResources, setRemainingResources]:any = React.useState(0); + + function createData(stage:any, taskCount:any, g1Planned:any, g1Actual:any, g2Planned:any, g2Actual:any, g3Planned:any, g3Actual:any, g4Planned:any, g4Actual:any, g5Planned:any, g5Actual:any, totalPlanned:any, totalActual:any, task:any) { + return { + stage, + taskCount, + g1Planned, + g1Actual, + g2Planned, + g2Actual, + g3Planned, + g3Actual, + g4Planned, + g4Actual, + g5Planned, + g5Actual, + totalPlanned, + totalActual, + task:task + } + } + + function createTaskData(stage:any, taskCount:any, g1Planned:any, g1Actual:any, g2Planned:any, g2Actual:any, g3Planned:any, g3Actual:any, g4Planned:any, g4Actual:any, g5Planned:any, g5Actual:any, totalPlanned:any, totalActual:any) { + return { + stage, + taskCount, + g1Planned, + g1Actual, + g2Planned, + g2Actual, + g3Planned, + g3Actual, + g4Planned, + g4Actual, + g5Planned, + g5Actual, + totalPlanned, + totalActual, + } + } + + const task1Rows:any = [ + {stage:"1.1 Preparation of preliminary...",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {stage:"1.2 Cash flow forecast",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {stage:"1.3 Cost studies for alterative design solutions",taskCount:"-",g1Planned:"-",g1Actual:"115.00",g2Planned:"-", g2Actual:"36.00", g3Planned:"-", g3Actual:"28.00", g4Planned: "-", g4Actual:"7.00", g5Planned:"-", g5Actual:"1.75", totalPlanned:"-", totalActual:"188.00"}, + {stage:"1.4 Attend design co-ordiantion / project",taskCount:"-",g1Planned:"-",g1Actual:"29.00",g2Planned:"-", g2Actual:"9.00", g3Planned:"-", g3Actual:"7.00", g4Planned: "-", g4Actual:"2.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"48.00"}, + {stage:"1.5 Prepare / Review RIC",taskCount:"-",g1Planned:"-",g1Actual:"88.00",g2Planned:"-", g2Actual:"27.00", g3Planned:"-", g3Actual:"21.00", g4Planned: "-", g4Actual:"5.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"141.75"} + ] + + const task2Rows:any = [ + ] + + const task3Rows:any = [ + ] + + const task4Rows:any = [ + ] + + const task5Rows:any = [ + ] + + const task6Rows:any = [ + ] + + const rows = [ + createData("Stage 1 - Design & Cost Planning / Estimating","5","576.00","576.00","192.00", "180.00", "144.00", "140.00", "38.40", "38.00", "9.60", "9.75", "960.00", "943.75",task1Rows), + createData("Stage 2 - Tender Documentation","11", "384.00", "382.00", "128.00", "130.00", "96.00", "79.00", "25.60", "25.00", "6.40", "4.00", "640.00", "620.00",task2Rows), + createData("Stage 3 - Tender Analysis & Report & Contract Documentation","7", "384.00", "300.00", "128.00", "130.00", "96.00", "79.00", "25.60", "25.00", "6.40", "4.00", "640.00", "538.00",task3Rows), + createData("Stage 4 - Construction", "13", "480.00", "400.00", "160.00", "160.00", "120.00", "128.00", "32.00", "25.00", "8.00", "3.00", "800.00", "716.00",task4Rows), + createData("Stage 5 - Miscellaneous", "4", "96.00", "-", "32.00", "-", "24.00", "-0", "6.40", "-", "1.600", "-", "160.00", "-",task5Rows), + createData("","Total", "1920.00", "1658.00", "640.00", "600.00", "480.00", "426.00", "128.00", "113.00", "32.00", "20.75", "3,200.00", "2817.75",task6Rows), + ]; + + // const taskRows = [ + // createTaskData("1.1 Preparation of preliminary...","-","-","172.00","-","54.00","-","42.00","-","12.00","-","3.00","-","283.00"), + // ]; + + function Row(props:any) { + const { row } = props; + const [open, setOpen] = React.useState(false); + + return ( + + *': { borderBottom: 'unset' } }}> + + {row.task.length > 0 && ( + setOpen(!open)} + > + {open ? : } + + )} + + {row.stage} + {row.taskCount} + {row.g1Planned} + {row.g1Actual} + {row.g2Planned} + {row.g2Actual} + {row.g3Planned} + {row.g3Actual} + {row.g4Planned} + {row.g4Actual} + {row.g5Planned} + {row.g5Actual} + {row.totalPlanned} + {row.totalActual} + + {row.task.map((taskRow:any) => ( + <> + + + + + + + + + {taskRow.stage} + + + + + {taskRow.taskCount} + + + + + {taskRow.g1Planned} + + + + + {taskRow.g1Actual} + + + + + {taskRow.g2Planned} + + + + + {taskRow.g2Actual} + + + + + {taskRow.g3Planned} + + + + + {taskRow.g3Actual} + + + + + {taskRow.g4Planned} + + + + + {taskRow.g4Actual} + + + + + {taskRow.g5Planned} + + + + + {taskRow.g5Actual} + + + + + {taskRow.totalPlanned} + + + + + {taskRow.totalActual} + + + + + ))} + {/* + + + + {row.task.map((taskRow:any) => ( + + + {taskRow.stage} + {taskRow.taskCount} + {taskRow.g1Planned} + {taskRow.g1Actual} + {taskRow.g2Planned} + {taskRow.g2Actual} + {taskRow.g3Planned} + {taskRow.g3Actual} + {taskRow.g4Planned} + {taskRow.g4Actual} + {taskRow.g5Planned} + {taskRow.g5Actual} + {taskRow.totalPlanned} + {taskRow.totalActual} + + ))} + + + + */} + + ); + } + + useEffect(() => { + setProjectName("C-1001-001 - Consultancy Project A") + const fee = 2000000 + setProjectFee(fee.toLocaleString()) + setStatus("Within Budget / Overconsumption") + const plannedResourcesInt = 3200 + setPlannedResources(plannedResourcesInt.toLocaleString()) + const actualResourcesSpentInt = 2817.75 + setActualResourcesSpent(actualResourcesSpentInt.toLocaleString()) + const remainingResourcesInt = 382.25 + setRemainingResources(remainingResourcesInt.toLocaleString()) + }, []) + + const projectResourcesRows = [ + {id: 1,stage:"Stage 1 - Design & Cost Planning / Estimating",taskCount:"5",g1Planned:"576.00",g1Actual:"576.00",g2Planned:"192.00", g2Actual:"180.00", g3Planned:"144.00", g3Actual:"140.00", g4Planned: "38.40", g4Actual:"38S.00", g5Planned:"9.60", g5Actual:"9.75", totalPlanned:"960.00", totalActual:"943.75"}, + {id: 2,stage:"1.1 Preparation of preliminary...",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {id: 3,stage:"1.2 Cash flow forecast",taskCount:"-",g1Planned:"-",g1Actual:"172.00",g2Planned:"-", g2Actual:"54.00", g3Planned:"-", g3Actual:"42.00", g4Planned: "-", g4Actual:"12.00", g5Planned:"-", g5Actual:"3.00", totalPlanned:"-", totalActual:"283.00"}, + {id: 4,stage:"1.3 Cost studies for alterative design solutions",taskCount:"-",g1Planned:"-",g1Actual:"115.00",g2Planned:"-", g2Actual:"36.00", g3Planned:"-", g3Actual:"28.00", g4Planned: "-", g4Actual:"7.00", g5Planned:"-", g5Actual:"1.75", totalPlanned:"-", totalActual:"188.00"}, + {id: 5,stage:"1.4 Attend design co-ordiantion / project",taskCount:"-",g1Planned:"-",g1Actual:"29.00",g2Planned:"-", g2Actual:"9.00", g3Planned:"-", g3Actual:"7.00", g4Planned: "-", g4Actual:"2.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"48.00"}, + {id: 6,stage:"1.5 Prepare / Review RIC",taskCount:"-",g1Planned:"-",g1Actual:"88.00",g2Planned:"-", g2Actual:"27.00", g3Planned:"-", g3Actual:"21.00", g4Planned: "-", g4Actual:"5.00", g5Planned:"-", g5Actual:"1.00", totalPlanned:"-", totalActual:"141.75"}, + {id: 7,stage:"Stage 2 - Tender Documentation",taskCount:"11",g1Planned:"384.00",g1Actual:"382.00",g2Planned:"128.00", g2Actual:"130.00", g3Planned:"96.00", g3Actual:"79.00", g4Planned: "25.60", g4Actual:"25.00", g5Planned:"6.40", g5Actual:"4.00", totalPlanned:"640.00", totalActual:"620.00"}, + {id: 8,stage:"Stage 3 - Tender Analysis & Report & Contract Documentation",taskCount:"7",g1Planned:"384.00",g1Actual:"300.00",g2Planned:"128.00", g2Actual:"130.00", g3Planned:"96.00", g3Actual:"79.00", g4Planned: "25.60", g4Actual:"25.00", g5Planned:"6.40", g5Actual:"4.00", totalPlanned:"640.00", totalActual:"538.00"}, + {id: 9,stage:"Stage 4 - Construction",taskCount:"13",g1Planned:"480.00",g1Actual:"400.00",g2Planned:"160.00", g2Actual:"160.00", g3Planned:"120.00", g3Actual:"128.00", g4Planned: "32.00", g4Actual:"25.00", g5Planned:"8.00", g5Actual:"3.00", totalPlanned:"800.00", totalActual:"716.00"}, + {id: 10,stage:"Stage 5 - Miscellaneous",taskCount:"4",g1Planned:"96.00",g1Actual:"-",g2Planned:"32.00", g2Actual:"-", g3Planned:"24.00", g3Actual:"-0", g4Planned: "6.40", g4Actual:"-", g5Planned:"1.600", g5Actual:"-", totalPlanned:"160.00", totalActual:"-"}, + {id: 11,stage:"",taskCount:"Total",g1Planned:"1920.00",g1Actual:"1658.00",g2Planned:"640.00", g2Actual:"600.00", g3Planned:"480.00", g3Actual:"426.00", g4Planned: "128.00", g4Actual:"113.00", g5Planned:"32.00", g5Actual:"20.75", totalPlanned:"3,200.00", totalActual:"2817.75"}, + ] + +const columns2 = [ + { + id: 'stage', + field: 'stage', + headerName: "Stage", + flex: 2, + }, + { + id: 'taskCount', + field: 'taskCount', + headerName: "Task Count", + flex: 0.5, + }, + { + id: 'g1Planned', + field: 'g1Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g1Actual', + field: 'g1Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g2Planned', + field: 'g2Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g2Actual', + field: 'g2Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g3Planned', + field: 'g3Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g3Actual', + field: 'g3Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g4Planned', + field: 'g4Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g4Actual', + field: 'g4Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'g5Planned', + field: 'g5Planned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'g5Actual', + field: 'g5Actual', + headerName: "Actual", + flex: 0.7, + }, + { + id: 'totalPlanned', + field: 'totalPlanned', + headerName: "Planned", + flex: 0.7, + }, + { + id: 'totalActual', + field: 'totalActual', + headerName: "Actual", + flex: 0.7, + }, +]; + + const columnGroupingModel = [ + { + groupId: 'G1', + children: [{ field: 'g1Planned' },{ field: 'g1Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G2', + children: [{ field: 'g2Planned' },{ field: 'g2Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G3', + children: [{ field: 'g3Planned' },{ field: 'g3Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G4', + children: [{ field: 'g4Planned' },{ field: 'g4Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'G5', + children: [{ field: 'g5Planned' },{ field: 'g5Actual' }], + headerClassName: 'groupColor', + }, + { + groupId: 'Total', + children: [{ field: 'totalPlanned' },{ field: 'totalActual' }], + headerClassName: 'totalGroupColor', + }, + ]; + + return ( + + + +
+
+
+ + Project + +
+
+ {projectName} +
+
+
+
+ + Project Fee + +
+
+ HKD {projectFee} +
+
+
+
+ + Status + +
+
+ {status} +
+
+
+
+ + Planned Resources + +
+
+ {plannedResources} Manhours +
+
+
+
+ + Actual Resources Spent + +
+
+ {actualResourcesSpent} Manhours +
+
+
+
+ + Remaining Resources + +
+
+ {remainingResources} Manhours +
+
+
+ {/*
+ +
*/} +
+ + + + + + + + + G1 + + + G2 + + + G3 + + + G4 + + + G5 + + + Total + + + + + Stage + Task Count + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + Planned + Actual + + + + {rows.map((row) => ( + + ))} + +
+
+
+
+
+ ); +}; + +export default ProjectResourceSummary; diff --git a/src/components/ProjectResourceSummary/index.ts b/src/components/ProjectResourceSummary/index.ts new file mode 100644 index 0000000..056f6f9 --- /dev/null +++ b/src/components/ProjectResourceSummary/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectResourceSummary"; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx new file mode 100644 index 0000000..d8efb61 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearch.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { ProjectResult } from "@/app/api/projects"; +import React, { useMemo, useState, useCallback } from "react"; +import SearchBox, { Criterion } from "../SearchBox"; +import { useTranslation } from "react-i18next"; +import SearchResults, { Column } from "../SearchResults"; +import { ResourceSummaryResult } from "@/app/api/resourcesummary"; +import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; +import ProjectResourceSummary from "@/components/ProjectResourceSummary"; +import ArticleIcon from '@mui/icons-material/Article'; + +interface Props { + projects: ResourceSummaryResult[]; +} +type SearchQuery = Partial>; +type SearchParamNames = keyof SearchQuery; + + +const ProjectResourceSummarySearch: React.FC = ({ projects }) => { + const { t } = useTranslation("projects"); + const searchParams = useSearchParams() + // If project searching is done on the server-side, then no need for this. + const [filteredProjects, setFilteredProjects] = useState(projects); + + const searchCriteria: Criterion[] = useMemo( + () => [ + { label: "Project Code", paramName: "projectCode", type: "text" }, + { label: "Project Name", paramName: "projectName", type: "text" }, + { label: "Client Code", paramName: "clientCode", type: "text" }, + { label: "Client Name", paramName: "clientName", type: "text" }, + ], + [t], + ); + + const onTaskClick = useCallback((resourceSummaryResult: ResourceSummaryResult) => { + console.log(resourceSummaryResult) + }, []); + + + const columns = useMemo[]>( + () => [ + { + name: "id", + label: t("View"), + onClick: onTaskClick, + buttonIcon: , + }, + { name: "projectCode", label: t("Project Code") }, + { name: "projectName", label: t("Project Name") }, + { name: "clientCodeAndName", label: t("Client Code And Name") }, + ], + [onTaskClick, t], + // [t], + ); + + return ( + <> + { + console.log(query); + }} + /> + + items={filteredProjects} + columns={columns} + /> + + + ); +}; + +export default ProjectResourceSummarySearch; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx new file mode 100644 index 0000000..b6d4bc1 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchLoading.tsx @@ -0,0 +1,40 @@ +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 ProjectResourceSummarySearchLoading: React.FC = () => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ProjectResourceSummarySearchLoading; diff --git a/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx new file mode 100644 index 0000000..068debc --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/ProjectResourceSummarySearchWrapper.tsx @@ -0,0 +1,20 @@ +import { fetchResourceSummary } from "@/app/api/resourcesummary"; +import React from "react"; +import ProjectResourceSummarySearch from "./ProjectResourceSummarySearch"; +import ProjectResourceSummarySearchLoading from "./ProjectResourceSummarySearchLoading"; + +interface SubComponents { + Loading: typeof ProjectResourceSummarySearchLoading; +} + +const ProjectResourceSummarySearchWrapper: React.FC & SubComponents = async () => { + const clentprojects = await fetchResourceSummary(); + + return ; +}; + +ProjectResourceSummarySearchWrapper.Loading = ProjectResourceSummarySearchLoading; + +export default ProjectResourceSummarySearchWrapper; + + diff --git a/src/components/ProjectResourceSummarySearch/index.ts b/src/components/ProjectResourceSummarySearch/index.ts new file mode 100644 index 0000000..98ec034 --- /dev/null +++ b/src/components/ProjectResourceSummarySearch/index.ts @@ -0,0 +1 @@ +export { default } from "./ProjectResourceSummarySearchWrapper"; From 529c26d114239b6e9ab810a72df99c7c551046f6 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Apr 2024 18:19:35 +0800 Subject: [PATCH 04/11] update gen EX02 report --- src/app/api/reports/actions.ts | 9 ++++++++- src/app/utils/commonUtil.ts | 12 ++---------- src/app/utils/fetchUtil.ts | 12 +++++++----- .../GenerateEX02ProjectCashFlowReport.tsx | 14 ++++---------- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index e0db4c4..6a7e70c 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -4,8 +4,13 @@ import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil"; import { EX02ProjectCashFlowReportRequest } from "."; import { BASE_API_URL } from "@/config/api"; +export interface FileResponse { + filename: string; + blobValue: Uint8Array; +} + export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowReportRequest) => { - const reportBlob = await serverFetchBlob( + const reportBlob = await serverFetchBlob( `${BASE_API_URL}/reports/EX02-ProjectCashFlowReport`, { method: "POST", @@ -14,5 +19,7 @@ export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowRe }, ); + console.log(reportBlob) + return reportBlob }; \ No newline at end of file diff --git a/src/app/utils/commonUtil.ts b/src/app/utils/commonUtil.ts index f0cc208..72d4a56 100644 --- a/src/app/utils/commonUtil.ts +++ b/src/app/utils/commonUtil.ts @@ -22,16 +22,8 @@ export const dateInRange = (currentDate: string, startDate: string, endDate: str } } -function s2ab(s: string) { - var buf = new ArrayBuffer(s.length); - var view = new Uint8Array(buf); - for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF; - return buf; -} - -export const downloadFile = (blob: Blob | string, type: string, filename: string) => { - - const url = URL.createObjectURL(typeof blob === "string" ? new Blob([blob], { type: type }) : blob); +export const downloadFile = (blobData: Uint8Array, filename: string) => { + const url = URL.createObjectURL(new Blob([blobData])); const link = document.createElement("a"); link.href = url; link.setAttribute("download", filename); diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index fa11529..7e9ab5f 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -8,6 +8,7 @@ export const serverFetch: typeof fetch = async (input, init) => { const session = await getServerSession(authOptions); const accessToken = session?.accessToken; + console.log(accessToken) return fetch(input, { ...init, headers: { @@ -56,15 +57,16 @@ export async function serverFetchWithNoContent(...args: FetchParams) { } } -export async function serverFetchBlob(...args: FetchParams) { +export async function serverFetchBlob(...args: FetchParams) { const response = await serverFetch(...args); if (response.ok) { console.log(response) - const blob = await response.blob() - const blobText = await blob.text(); - const blobType = await blob.type; - return {filename: response.headers.get("filename"), blobText: blobText, blobType: blobType}; + // const blob = await response.blob() + // const blobText = await blob.text(); + // const blobType = await blob.type; + const blobValue = (await response.body?.getReader().read())!!.value!! + return {filename: response.headers.get("filename"), blobValue: blobValue} as T; } else { switch (response.status) { case 401: diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx index d0bc75f..43b5e5d 100644 --- a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx @@ -7,6 +7,7 @@ import { ProjectResult } from "@/app/api/projects"; import { EX02ProjectCashFlowReportFilter } from "@/app/api/reports"; import { fetchEX02ProjectCashFlowReport } from "@/app/api/reports/actions"; import { downloadFile } from "@/app/utils/commonUtil"; +import { BASE_API_URL } from "@/config/api"; interface Props { projects: ProjectResult[]; @@ -21,7 +22,7 @@ const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { const searchCriteria: Criterion[] = useMemo( () => [ - { label: t("Project"), paramName: "project", type: "select", options: projectCombo}, + { label: t("Project"), paramName: "project", type: "select", options: projectCombo }, ], [t], ); @@ -32,17 +33,10 @@ const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { criteria={searchCriteria} onSearch={async (query) => { const projectIndex = projectCombo.findIndex(project => project === query.project) - const response = await fetchEX02ProjectCashFlowReport({projectId: projects[projectIndex].id}) - console.log(response) + const response = await fetchEX02ProjectCashFlowReport({ projectId: projects[projectIndex].id }) if (response) { - downloadFile(response.blobText, response.blobType, response.filename!!) + downloadFile(new Uint8Array(response.blobValue), response.filename!!) } - - // const url = URL.createObjectURL(response.blob); - // const link = document.createElement("a"); - // link.href = url; - // link.setAttribute("download", "abc.xlsx"); - // link.click(); }} /> From 8771ce179a13deaa46ae83444f38edadade85682 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Mon, 29 Apr 2024 18:35:54 +0800 Subject: [PATCH 05/11] update report (but sometimes cannot open) --- src/app/api/reports/actions.ts | 2 -- src/app/utils/fetchUtil.ts | 10 ++++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index 6a7e70c..be74c6f 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -19,7 +19,5 @@ export const fetchEX02ProjectCashFlowReport = async (data: EX02ProjectCashFlowRe }, ); - console.log(reportBlob) - return reportBlob }; \ No newline at end of file diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 7e9ab5f..8f563e2 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -16,7 +16,7 @@ export const serverFetch: typeof fetch = async (input, init) => { ...(accessToken ? { Authorization: `Bearer ${accessToken}`, - Accept: "application/json" + Accept: "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" } : {}), }, @@ -61,12 +61,14 @@ export async function serverFetchBlob(...args: FetchParams) { const response = await serverFetch(...args); if (response.ok) { - console.log(response) // const blob = await response.blob() // const blobText = await blob.text(); // const blobType = await blob.type; - const blobValue = (await response.body?.getReader().read())!!.value!! - return {filename: response.headers.get("filename"), blobValue: blobValue} as T; + const readBody = await response.body?.getReader().read() + const bodyValue = readBody!!.value!! + + console.log(bodyValue) + return {filename: response.headers.get("filename"), blobValue: bodyValue} as T; } else { switch (response.status) { case 401: From 1f69f582be430bf36acfaf40f2e569c8b29afe3f Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 30 Apr 2024 12:08:33 +0800 Subject: [PATCH 06/11] fix sometimes cannot open the file --- src/app/utils/fetchUtil.ts | 39 +++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 8f563e2..9fa878e 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -61,14 +61,43 @@ export async function serverFetchBlob(...args: FetchParams) { const response = await serverFetch(...args); if (response.ok) { + const body = response.body + // console.log(body) + // console.log(body?.tee()[0].getReader()) + + + const reader = body?.getReader() + let finalUInt8Array = new Uint8Array() + let done = false + + // Read some more, and call this function again + while (!done) { + // Result objects contain two properties: + // done - true if the stream has already given you all its data. + // value - some data. Always undefined when done is true. + const read = await reader?.read() + + if (read?.done) { + done = true + } else { + // value for fetch streams is a Uint8Array + finalUInt8Array = new Uint8Array(read?.value.length!!) + finalUInt8Array.set(read?.value!!) + } + } + + // const bodyRead = await reader?.read() + // const bodyValue = bodyRead?.value + // const blob = await response.blob() // const blobText = await blob.text(); // const blobType = await blob.type; - const readBody = await response.body?.getReader().read() - const bodyValue = readBody!!.value!! - - console.log(bodyValue) - return {filename: response.headers.get("filename"), blobValue: bodyValue} as T; + + // console.log(bodyReader) + // console.log(finalUInt8Array) + // console.log(bodyValue) + + return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T; } else { switch (response.status) { case 401: From 1c3cfafe5bb732fcf0cfc72c92dc102b114d92e8 Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 30 Apr 2024 14:46:26 +0800 Subject: [PATCH 07/11] update --- src/app/utils/fetchUtil.ts | 36 ++++++++++++++----- .../GenerateEX02ProjectCashFlowReport.tsx | 13 ++++--- src/components/SearchBox/SearchBox.tsx | 3 +- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 9fa878e..4c96d1e 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -70,23 +70,41 @@ export async function serverFetchBlob(...args: FetchParams) { let finalUInt8Array = new Uint8Array() let done = false - // Read some more, and call this function again while (!done) { - // Result objects contain two properties: - // done - true if the stream has already given you all its data. - // value - some data. Always undefined when done is true. const read = await reader?.read() + // version 1 if (read?.done) { done = true } else { - // value for fetch streams is a Uint8Array - finalUInt8Array = new Uint8Array(read?.value.length!!) - finalUInt8Array.set(read?.value!!) + const tempUInt8Array = new Uint8Array(finalUInt8Array.length + read?.value.length!!) + tempUInt8Array.set(finalUInt8Array) + tempUInt8Array.set(read?.value!!, finalUInt8Array.length) + finalUInt8Array = new Uint8Array(tempUInt8Array.length!!) + finalUInt8Array.set(tempUInt8Array) + + // console.log("1", finalUInt8Array) } } - // const bodyRead = await reader?.read() + // version 2 & return bodyRead + // const bodyRead = reader?.read().then(function processText({ done, value }): any { + // // Result objects contain two properties: + // // done - true if the stream has already given you all its data. + // // value - some data. Always undefined when done is true. + // if (done) { + // console.log("Stream complete"); + // return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T;; + // } + + // // value for fetch streams is a Uint8Array + // finalUInt8Array = new Uint8Array(value.length) + // finalUInt8Array.set(value) + + // console.log(finalUInt8Array) + // // Read some more, and call this function again + // return reader.read().then(processText); + // }) // const bodyValue = bodyRead?.value // const blob = await response.blob() @@ -94,7 +112,7 @@ export async function serverFetchBlob(...args: FetchParams) { // const blobType = await blob.type; // console.log(bodyReader) - // console.log(finalUInt8Array) + // console.log("2", finalUInt8Array) // console.log(bodyValue) return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T; diff --git a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx index 43b5e5d..7aec1c2 100644 --- a/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx +++ b/src/components/GenerateEX02ProjectCashFlowReport/GenerateEX02ProjectCashFlowReport.tsx @@ -22,7 +22,7 @@ const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { const searchCriteria: Criterion[] = useMemo( () => [ - { label: t("Project"), paramName: "project", type: "select", options: projectCombo }, + { label: t("Project"), paramName: "project", type: "select", options: projectCombo, needAll: false}, ], [t], ); @@ -32,10 +32,13 @@ const GenerateEX02ProjectCashFlowReport: React.FC = ({ projects }) => { { - const projectIndex = projectCombo.findIndex(project => project === query.project) - const response = await fetchEX02ProjectCashFlowReport({ projectId: projects[projectIndex].id }) - if (response) { - downloadFile(new Uint8Array(response.blobValue), response.filename!!) + + if (query.project.length > 0 && query.project.toLocaleLowerCase() !== "all") { + const projectIndex = projectCombo.findIndex(project => project === query.project) + const response = await fetchEX02ProjectCashFlowReport({ projectId: projects[projectIndex].id }) + if (response) { + downloadFile(new Uint8Array(response.blobValue), response.filename!!) + } } }} /> diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index 26914fe..5ea1690 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -36,6 +36,7 @@ interface TextCriterion extends BaseCriterion { interface SelectCriterion extends BaseCriterion { type: "select"; options: string[]; + needAll?: boolean; } interface DateRangeCriterion extends BaseCriterion { @@ -134,7 +135,7 @@ function SearchBox({ onChange={makeSelectChangeHandler(c.paramName)} value={inputs[c.paramName]} > - {t("All")} + {!(c.needAll === false) && {t("All")}} {c.options.map((option, index) => ( {t(option)} From 2fd5beba0a4fa169fe552e7e5f27ceaf7ecaa981 Mon Sep 17 00:00:00 2001 From: "MSI\\derek" Date: Tue, 30 Apr 2024 17:03:52 +0800 Subject: [PATCH 08/11] staff multiple skills --- src/components/CreateStaff/CreateStaff.tsx | 2 +- .../CustomInputForm/CustomInputForm.tsx | 68 ++++++++++++++++++- src/components/EditStaff/EditStaff.tsx | 29 ++++++-- 3 files changed, 88 insertions(+), 11 deletions(-) diff --git a/src/components/CreateStaff/CreateStaff.tsx b/src/components/CreateStaff/CreateStaff.tsx index 312f18e..94e7f71 100644 --- a/src/components/CreateStaff/CreateStaff.tsx +++ b/src/components/CreateStaff/CreateStaff.tsx @@ -190,7 +190,7 @@ const CreateStaff: React.FC = ({ Title }) => { { id: "skillSetId", label: t("Skillset"), - type: "combo-Obj", + type: "multiSelect-Obj", options: skillCombo || [], required: false, }, diff --git a/src/components/CustomInputForm/CustomInputForm.tsx b/src/components/CustomInputForm/CustomInputForm.tsx index 9497208..417c6f1 100644 --- a/src/components/CustomInputForm/CustomInputForm.tsx +++ b/src/components/CustomInputForm/CustomInputForm.tsx @@ -15,6 +15,7 @@ import { Checkbox, FormControlLabel, Button, + Chip, } from "@mui/material"; import { DataGrid, GridColDef, GridRowSelectionModel } from "@mui/x-data-grid"; import { darken, lighten, styled } from "@mui/material/styles"; @@ -31,6 +32,7 @@ import { useCallback, useEffect, useState } from "react"; import { Check, Close, RestartAlt } from "@mui/icons-material"; import { NumericFormat, NumericFormatProps } from "react-number-format"; import * as React from "react"; +import CancelIcon from "@mui/icons-material/Cancel"; interface Options { id: any; @@ -286,7 +288,7 @@ const CustomInputForm: React.FC = ({ ); } else if (field.type === "multiDate") { - console.log(dayjs(field.value)) + // console.log(dayjs(field.value)) return ( @@ -343,8 +345,6 @@ const CustomInputForm: React.FC = ({ id={field.id} value={value} onChange={(event) => { - console.log(event); - console.log(event.target); onChange(event.target.value); const newValue = event.target.value; const selectedOption = field.options?.find( @@ -379,6 +379,68 @@ const CustomInputForm: React.FC = ({ ); + } else if (field.type === "multiSelect-Obj") { + return ( + + + {field.label} + ( + + )} + /> + + + ); } else if (field.type === "numeric") { return ( diff --git a/src/components/EditStaff/EditStaff.tsx b/src/components/EditStaff/EditStaff.tsx index bcc7346..0d62e28 100644 --- a/src/components/EditStaff/EditStaff.tsx +++ b/src/components/EditStaff/EditStaff.tsx @@ -15,8 +15,15 @@ import { fetchSkillCombo } from "@/app/api/skill/actions"; import { fetchSalaryCombo } from "@/app/api/salarys/actions"; // import { Field } from "react-hook-form"; -interface dataType { - [key: string]: any; + +interface skill { + id: number; + name: string; + code: string; +} +interface skillObj { + id: number; + skill: skill; } interface Options { @@ -113,7 +120,9 @@ const EditStaff: React.FC = async () => { if (data) setGradeCombo(data.records); }); fetchSkillCombo().then((data) => { - if (data) setSkillCombo(data.records); + if (data) { + }setSkillCombo(data.records); + console.log(data.records) }); fetchSalaryCombo().then((data) => { if (data) setSalaryCombo(data.records); @@ -127,6 +136,10 @@ const EditStaff: React.FC = async () => { console.log(id) fetchStaffEdit(id).then((staff) => { console.log(staff.data); + const skillset = staff.data.skillset + console.log(skillset); + const skillIds = skillset.map((item: skillObj) => item.skill.id); + console.log(skillIds) const data = staff.data; ///////////////////// list 1 ///////////////////// const list1 = keyOrder1 @@ -181,15 +194,17 @@ const EditStaff: React.FC = async () => { label: t(`Grade`), type: "combo-Obj", options: gradeCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: data[key]?.id ?? "", }; case "skill": + console.log(skillIds) return { id: `${key}SetId`, label: t(`Skillset`), - type: "combo-Obj", + type: "multiSelect-Obj", options: skillCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: skillIds ?? [], + //array problem }; case "currentPosition": return { @@ -206,7 +221,7 @@ const EditStaff: React.FC = async () => { label: t(`Salary Point`), type: "combo-Obj", options: salaryCombo, - value: data[key] !== null ? data[key].id ?? "" : "", + value: data[key]?.id ?? "", required: true, }; // case "hourlyRate": From 301bacdec669b2c7f70d3475007ae3ca7d2d9449 Mon Sep 17 00:00:00 2001 From: Wayne Date: Tue, 30 Apr 2024 18:41:20 +0900 Subject: [PATCH 09/11] Fetch assigned projects --- package-lock.json | 172 +++++++++++++++++- src/app/api/projects/index.ts | 14 ++ src/app/utils/fetchUtil.ts | 39 ++-- .../TimesheetModal/TimesheetModal.tsx | 5 +- .../TimesheetTable/EntryInputTable.tsx | 128 ++----------- .../TimesheetTable/TimesheetTable.tsx | 25 ++- .../UserWorkspacePage/AssignedProjects.tsx | 53 +----- .../UserWorkspacePage/ProjectGrid.tsx | 23 +-- .../UserWorkspacePage/UserWorkspacePage.tsx | 10 +- .../UserWorkspaceWrapper.tsx | 64 +------ 10 files changed, 265 insertions(+), 268 deletions(-) diff --git a/package-lock.json b/package-lock.json index 84bf9b7..398e982 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@emotion/cache": "^11.11.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@faker-js/faker": "^8.4.1", "@fontsource/inter": "^5.0.16", "@fontsource/plus-jakarta-sans": "^5.0.18", "@mui/icons-material": "^5.15.0", @@ -37,7 +38,8 @@ "react-select": "^5.8.0", "reactstrap": "^9.2.2", "styled-components": "^6.1.8", - "sweetalert2": "^11.10.3" + "sweetalert2": "^11.10.3", + "xlsx-js-style": "^1.2.0" }, "devDependencies": { "@types/lodash": "^4.14.202", @@ -1933,6 +1935,21 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@faker-js/faker": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.4.1.tgz", + "integrity": "sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.0.tgz", @@ -3530,6 +3547,21 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", + "integrity": "sha512-/vUqU/UY4MVeFsg+SsK6c+/05RZXIHZMGJA+PX5JyWI0ZRcBpupnRuPLU/NXXoFwMYCPCoxIfElM2eS+DUXCqQ==", + "dependencies": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + }, + "bin": { + "adler32": "bin/adler32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -4150,6 +4182,26 @@ } ] }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/cfb/node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -4236,6 +4288,26 @@ "node": ">=6" } }, + "node_modules/codepage": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", + "integrity": "sha512-iz3zJLhlrg37/gYRWgEPkaFTtzmnEv1h+r7NgZum2lFElYQPi0/5bnmuDfODHxfp0INEfnRqyfyeIJDbb7ahRw==", + "dependencies": { + "commander": "~2.14.1", + "exit-on-epipe": "~1.0.1" + }, + "bin": { + "codepage": "bin/codepage.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/codepage/node_modules/commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -4316,6 +4388,17 @@ "node": ">=10" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5431,6 +5514,14 @@ "node": ">=0.8.x" } }, + "node_modules/exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -5476,6 +5567,11 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz", + "integrity": "sha512-Rr5QlUeGN1mbOHlaqcSYMKVpPbgLy0AWT/W0EHxA6NGI12yO1jpoui2zBBvU2G824ltM6Ut8BFgfHSBGfkmS0A==" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5599,6 +5695,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -7880,6 +7984,17 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==" }, + "node_modules/printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==", + "bin": { + "printj": "bin/printj.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8631,6 +8746,17 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -9853,6 +9979,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/workbox-background-sync": { "version": "6.6.0", "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.6.0.tgz", @@ -10238,6 +10380,34 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xlsx-js-style": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/xlsx-js-style/-/xlsx-js-style-1.2.0.tgz", + "integrity": "sha512-DDT4FXFSWfT4DXMSok/m3TvmP1gvO3dn0Eu/c+eXHW5Kzmp7IczNkxg/iEPnImbG9X0Vb8QhROda5eatSR/97Q==", + "dependencies": { + "adler-32": "~1.2.0", + "cfb": "^1.1.4", + "codepage": "~1.14.0", + "commander": "~2.17.1", + "crc-32": "~1.2.0", + "exit-on-epipe": "~1.0.1", + "fflate": "^0.3.8", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx-js-style/node_modules/commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 0c7209c..7bfb067 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -59,6 +59,11 @@ export interface AssignedProject { endDate: string; }; }; + // Manhour info + hoursSpent: number; + hoursSpentOther: number; + hoursAllocated: number; + hoursAllocatedOther: number; } export const preloadProjects = () => { @@ -131,3 +136,12 @@ export const fetchProjectWorkNatures = cache(async () => { next: { tags: ["projectWorkNatures"] }, }); }); + +export const fetchAssignedProjects = cache(async () => { + return serverFetchJson( + `${BASE_API_URL}/projects/assignedProjects`, + { + next: { tags: ["assignedProjects"] }, + }, + ); +}); diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index 4c96d1e..5060991 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -8,16 +8,17 @@ export const serverFetch: typeof fetch = async (input, init) => { const session = await getServerSession(authOptions); const accessToken = session?.accessToken; - console.log(accessToken) + console.log(accessToken); return fetch(input, { ...init, headers: { ...init?.headers, ...(accessToken ? { - Authorization: `Bearer ${accessToken}`, - Accept: "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - } + Authorization: `Bearer ${accessToken}`, + Accept: + "application/json, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + } : {}), }, }); @@ -61,27 +62,28 @@ export async function serverFetchBlob(...args: FetchParams) { const response = await serverFetch(...args); if (response.ok) { - const body = response.body + const body = response.body; // console.log(body) // console.log(body?.tee()[0].getReader()) - - const reader = body?.getReader() - let finalUInt8Array = new Uint8Array() - let done = false + const reader = body?.getReader(); + let finalUInt8Array = new Uint8Array(); + let done = false; while (!done) { - const read = await reader?.read() + const read = await reader?.read(); // version 1 if (read?.done) { - done = true + done = true; } else { - const tempUInt8Array = new Uint8Array(finalUInt8Array.length + read?.value.length!!) - tempUInt8Array.set(finalUInt8Array) - tempUInt8Array.set(read?.value!!, finalUInt8Array.length) - finalUInt8Array = new Uint8Array(tempUInt8Array.length!!) - finalUInt8Array.set(tempUInt8Array) + const tempUInt8Array = new Uint8Array( + finalUInt8Array.length + read?.value.length!, + ); + tempUInt8Array.set(finalUInt8Array); + tempUInt8Array.set(read?.value!, finalUInt8Array.length); + finalUInt8Array = new Uint8Array(tempUInt8Array.length!); + finalUInt8Array.set(tempUInt8Array); // console.log("1", finalUInt8Array) } @@ -115,7 +117,10 @@ export async function serverFetchBlob(...args: FetchParams) { // console.log("2", finalUInt8Array) // console.log(bodyValue) - return { filename: response.headers.get("filename"), blobValue: finalUInt8Array } as T; + return { + filename: response.headers.get("filename"), + blobValue: finalUInt8Array, + } as T; } else { switch (response.status) { case 401: diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx index 055e0d9..d6146b0 100644 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ b/src/components/TimesheetModal/TimesheetModal.tsx @@ -16,11 +16,13 @@ import { FormProvider, useForm } from "react-hook-form"; import { RecordTimesheetInput } from "@/app/api/timesheets/actions"; import dayjs from "dayjs"; import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; +import { AssignedProject } from "@/app/api/projects"; interface Props { isOpen: boolean; onClose: () => void; timesheetType: "time" | "leave"; + assignedProjects: AssignedProject[]; } const modalSx: SxProps = { @@ -37,6 +39,7 @@ const TimesheetModal: React.FC = ({ isOpen, onClose, timesheetType, + assignedProjects, }) => { const { t } = useTranslation("home"); @@ -73,7 +76,7 @@ const TimesheetModal: React.FC = ({ marginBlock: 4, }} > - + - - + {Boolean(assignedProjects.length) && ( + + + + + )} - - - + {assignedProjects.length > 0 ? ( + <> + + + + + ) : ( + <> + + {t("You have no assigned projects!")} + + + )} ); }; From 6fdbe946101652d9058bccf43867d9b8578159cc Mon Sep 17 00:00:00 2001 From: "cyril.tsui" Date: Tue, 30 Apr 2024 18:13:43 +0800 Subject: [PATCH 11/11] update tasks (can edit now), staff, subsidiary, customer, user, rename customer, --- .../(main)/settings/customer/create/page.tsx | 4 +- .../(main)/settings/customer/edit/page.tsx | 4 +- src/app/(main)/tasks/create/page.tsx | 5 +- src/app/(main)/tasks/edit/page.tsx | 26 +++ src/app/(main)/tasks/page.tsx | 13 +- src/app/api/tasks/actions.ts | 30 ++- .../CreateTaskTemplate/CreateTaskTemplate.tsx | 191 +++++++++++------- src/components/CustomerDetail/index.ts | 1 - .../ContactInfo.tsx | 0 .../CustomerInfo.tsx | 0 .../CustomerSave.tsx} | 4 +- .../CustomerSaveWrapper.tsx} | 8 +- .../SubsidiaryAllocation.tsx | 0 src/components/CustomerSave/index.ts | 1 + src/components/StaffSearch/StaffSearch.tsx | 2 +- .../SubsidiaryDetailWrapper.tsx | 4 +- .../SubsidiarySearch/SubsidiarySearch.tsx | 2 +- .../TaskTemplateSearch/TaskTemplateSearch.tsx | 28 ++- src/components/TeamSearch/TeamSearch.tsx | 2 +- src/components/TransferList/TransferList.tsx | 2 +- src/components/UserSearch/UserSearch.tsx | 2 +- src/i18n/en/common.json | 2 + src/i18n/en/tasks.json | 27 +++ src/i18n/zh/common.json | 2 + src/i18n/zh/tasks.json | 27 +++ 25 files changed, 291 insertions(+), 96 deletions(-) create mode 100644 src/app/(main)/tasks/edit/page.tsx delete mode 100644 src/components/CustomerDetail/index.ts rename src/components/{CustomerDetail => CustomerSave}/ContactInfo.tsx (100%) rename src/components/{CustomerDetail => CustomerSave}/CustomerInfo.tsx (100%) rename src/components/{CustomerDetail/CustomerDetail.tsx => CustomerSave/CustomerSave.tsx} (99%) rename src/components/{CustomerDetail/CustomerDetailWrapper.tsx => CustomerSave/CustomerSaveWrapper.tsx} (73%) rename src/components/{CustomerDetail => CustomerSave}/SubsidiaryAllocation.tsx (100%) create mode 100644 src/components/CustomerSave/index.ts create mode 100644 src/i18n/en/tasks.json create mode 100644 src/i18n/zh/tasks.json diff --git a/src/app/(main)/settings/customer/create/page.tsx b/src/app/(main)/settings/customer/create/page.tsx index e0dc0e0..13b38eb 100644 --- a/src/app/(main)/settings/customer/create/page.tsx +++ b/src/app/(main)/settings/customer/create/page.tsx @@ -1,4 +1,4 @@ -import CustomerDetail from "@/components/CustomerDetail"; +import CustomerSave from "@/components/CustomerSave"; // import { preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { I18nProvider, getServerI18n } from "@/i18n"; @@ -16,7 +16,7 @@ const CreateCustomer: React.FC = async () => { <> {t("Create Customer")} - + ); diff --git a/src/app/(main)/settings/customer/edit/page.tsx b/src/app/(main)/settings/customer/edit/page.tsx index 004781b..7755403 100644 --- a/src/app/(main)/settings/customer/edit/page.tsx +++ b/src/app/(main)/settings/customer/edit/page.tsx @@ -1,5 +1,5 @@ import { fetchAllSubsidiaries, preloadAllCustomers } from "@/app/api/customer"; -import CustomerDetail from "@/components/CustomerDetail"; +import CustomerSave from "@/components/CustomerSave"; // import { preloadAllTasks } from "@/app/api/tasks"; import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { I18nProvider, getServerI18n } from "@/i18n"; @@ -18,7 +18,7 @@ const EditCustomer: React.FC = async () => { <> {t("Edit Customer")} - + ); diff --git a/src/app/(main)/tasks/create/page.tsx b/src/app/(main)/tasks/create/page.tsx index 656139f..262f624 100644 --- a/src/app/(main)/tasks/create/page.tsx +++ b/src/app/(main)/tasks/create/page.tsx @@ -3,6 +3,7 @@ import CreateTaskTemplate from "@/components/CreateTaskTemplate"; import { getServerI18n } from "@/i18n"; import Typography from "@mui/material/Typography"; import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; export const metadata: Metadata = { title: "Create Task Template", @@ -15,7 +16,9 @@ const Projects: React.FC = async () => { return ( <> {t("Create Task Template")} - + + + ); }; diff --git a/src/app/(main)/tasks/edit/page.tsx b/src/app/(main)/tasks/edit/page.tsx new file mode 100644 index 0000000..2b2c02c --- /dev/null +++ b/src/app/(main)/tasks/edit/page.tsx @@ -0,0 +1,26 @@ +import { preloadAllTasks } from "@/app/api/tasks"; +import CreateTaskTemplate from "@/components/CreateTaskTemplate"; +import { getServerI18n } from "@/i18n"; +import Typography from "@mui/material/Typography"; +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; + +export const metadata: Metadata = { + title: "Edit Task Template", +}; + +const TaskTemplates: React.FC = async () => { + const { t } = await getServerI18n("tasks"); + preloadAllTasks(); + + return ( + <> + {t("Edit Task Template")} + + + + + ); +}; + +export default TaskTemplates; diff --git a/src/app/(main)/tasks/page.tsx b/src/app/(main)/tasks/page.tsx index b9e9bf8..bf06dc2 100644 --- a/src/app/(main)/tasks/page.tsx +++ b/src/app/(main)/tasks/page.tsx @@ -8,13 +8,14 @@ import Typography from "@mui/material/Typography"; import { Metadata } from "next"; import Link from "next/link"; import { Suspense } from "react"; +import { I18nProvider } from "@/i18n"; export const metadata: Metadata = { title: "Tasks", }; const TaskTemplates: React.FC = async () => { - const { t } = await getServerI18n("projects"); + const { t } = await getServerI18n("tasks"); preloadTaskTemplates(); return ( @@ -34,12 +35,14 @@ const TaskTemplates: React.FC = async () => { LinkComponent={Link} href="/tasks/create" > - {t("Create Template")} + {t("Create Task Template")} - }> - - + + }> + + + ); }; diff --git a/src/app/api/tasks/actions.ts b/src/app/api/tasks/actions.ts index 862cc62..2c043be 100644 --- a/src/app/api/tasks/actions.ts +++ b/src/app/api/tasks/actions.ts @@ -1,6 +1,6 @@ "use server"; -import { serverFetchJson } from "@/app/utils/fetchUtil"; +import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { TaskTemplate } from "."; import { revalidateTag } from "next/cache"; @@ -9,11 +9,13 @@ export interface NewTaskTemplateFormInputs { code: string; name: string; taskIds: number[]; + + id: number | null; } export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { const newTaskTemplate = await serverFetchJson( - `${BASE_API_URL}/tasks/templates/new`, + `${BASE_API_URL}/tasks/templates/save`, { method: "POST", body: JSON.stringify(data), @@ -25,3 +27,27 @@ export const saveTaskTemplate = async (data: NewTaskTemplateFormInputs) => { return newTaskTemplate; }; + +export const fetchTaskTemplate = async (id: number) => { + const taskTemplate = await serverFetchJson( + `${BASE_API_URL}/tasks/templates/${id}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + return taskTemplate; +}; + +export const deleteTaskTemplate = async (id: number) => { + const taskTemplate = await serverFetchWithNoContent( + `${BASE_API_URL}/tasks/templates/${id}`, + { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + }, + ); + + return taskTemplate +}; diff --git a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx index 066c994..f7d5912 100644 --- a/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx +++ b/src/components/CreateTaskTemplate/CreateTaskTemplate.tsx @@ -10,15 +10,17 @@ import TransferList from "../TransferList"; import Button from "@mui/material/Button"; import Check from "@mui/icons-material/Check"; import Close from "@mui/icons-material/Close"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import React from "react"; import Stack from "@mui/material/Stack"; import { Task } from "@/app/api/tasks"; import { NewTaskTemplateFormInputs, + fetchTaskTemplate, saveTaskTemplate, } from "@/app/api/tasks/actions"; -import { SubmitHandler, useForm } from "react-hook-form"; +import { SubmitHandler, useFieldArray, useForm } from "react-hook-form"; +import { errorDialog, submitDialog, successDialog } from "../Swal/CustomAlerts"; interface Props { tasks: Task[]; @@ -27,6 +29,7 @@ interface Props { const CreateTaskTemplate: React.FC = ({ tasks }) => { const { t } = useTranslation(); + const searchParams = useSearchParams() const router = useRouter(); const handleCancel = () => { router.back(); @@ -49,6 +52,7 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { handleSubmit, setValue, watch, + resetField, formState: { errors, isSubmitting }, } = useForm({ defaultValues: { taskIds: [] } }); @@ -57,12 +61,56 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { return items.filter((item) => currentTaskIds.includes(item.id)); }, [currentTaskIds, items]); + const [refTaskTemplate, setRefTaskTemplate] = React.useState() + const id = searchParams.get('id') + + const fetchCurrentTaskTemplate = async () => { + try { + const taskTemplate = await fetchTaskTemplate(parseInt(id!!)) + + const defaultValues = { + id: parseInt(id!!), + code: taskTemplate.code ?? null, + name: taskTemplate.name ?? null, + taskIds: taskTemplate.tasks.map(task => task.id) ?? [], + } + + setRefTaskTemplate(defaultValues) + } catch (e) { + console.log(e) + } + } + + React.useLayoutEffect(() => { + if (id !== null && parseInt(id) > 0) fetchCurrentTaskTemplate() + }, [id]) + + React.useEffect(() => { + if (refTaskTemplate) { + setValue("taskIds", refTaskTemplate.taskIds) + resetField("code", { defaultValue: refTaskTemplate.code }) + resetField("name", { defaultValue: refTaskTemplate.name }) + setValue("id", refTaskTemplate.id) + } + }, [refTaskTemplate]) + const onSubmit: SubmitHandler = React.useCallback( async (data) => { try { setServerError(""); - await saveTaskTemplate(data); - router.replace("/tasks"); + submitDialog(async () => { + const response = await saveTaskTemplate(data); + + if (response?.id !== null && response?.id !== undefined && response?.id > 0) { + successDialog(t("Submit Success"), t).then(() => { + router.replace("/tasks"); + }) + } else { + errorDialog(t("Submit Fail"), t).then(() => { + return false + }) + } + }, t) } catch (e) { setServerError(t("An error has occurred. Please try again later.")); } @@ -71,72 +119,77 @@ const CreateTaskTemplate: React.FC = ({ tasks }) => { ); return ( - - - - {t("Task List Setup")} - - - - - - + { + (id === null || refTaskTemplate !== undefined) && + + + {t("Task List Setup")} + + + + + + + + + { + setValue( + "taskIds", + selectedTasks.map((item) => item.id), + ); + }} + allItemsLabel={t("Task Pool")} + selectedItemsLabel={t("Task List Template")} /> - - - { - setValue( - "taskIds", - selectedTasks.map((item) => item.id), - ); - }} - allItemsLabel={t("Task Pool")} - selectedItemsLabel={t("Task List Template")} - /> - - - {serverError && ( - - {serverError} - - )} - - - - - + + + { + serverError && ( + + {serverError} + + ) + } + + + + + } + ); }; diff --git a/src/components/CustomerDetail/index.ts b/src/components/CustomerDetail/index.ts deleted file mode 100644 index a8811e6..0000000 --- a/src/components/CustomerDetail/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./CustomerDetailWrapper"; \ No newline at end of file diff --git a/src/components/CustomerDetail/ContactInfo.tsx b/src/components/CustomerSave/ContactInfo.tsx similarity index 100% rename from src/components/CustomerDetail/ContactInfo.tsx rename to src/components/CustomerSave/ContactInfo.tsx diff --git a/src/components/CustomerDetail/CustomerInfo.tsx b/src/components/CustomerSave/CustomerInfo.tsx similarity index 100% rename from src/components/CustomerDetail/CustomerInfo.tsx rename to src/components/CustomerSave/CustomerInfo.tsx diff --git a/src/components/CustomerDetail/CustomerDetail.tsx b/src/components/CustomerSave/CustomerSave.tsx similarity index 99% rename from src/components/CustomerDetail/CustomerDetail.tsx rename to src/components/CustomerSave/CustomerSave.tsx index 88a99ad..fc2469e 100644 --- a/src/components/CustomerDetail/CustomerDetail.tsx +++ b/src/components/CustomerSave/CustomerSave.tsx @@ -42,7 +42,7 @@ const hasErrorsInTab = ( } }; -const CustomerDetail: React.FC = ({ +const CustomerSave: React.FC = ({ subsidiaries, customerTypes, }) => { @@ -277,4 +277,4 @@ const CustomerDetail: React.FC = ({ ); }; -export default CustomerDetail; \ No newline at end of file +export default CustomerSave; \ No newline at end of file diff --git a/src/components/CustomerDetail/CustomerDetailWrapper.tsx b/src/components/CustomerSave/CustomerSaveWrapper.tsx similarity index 73% rename from src/components/CustomerDetail/CustomerDetailWrapper.tsx rename to src/components/CustomerSave/CustomerSaveWrapper.tsx index 0206940..078f50a 100644 --- a/src/components/CustomerDetail/CustomerDetailWrapper.tsx +++ b/src/components/CustomerSave/CustomerSaveWrapper.tsx @@ -3,7 +3,7 @@ // import { fetchProjectCategories } from "@/app/api/projects"; // import { fetchTeamLeads } from "@/app/api/staff"; import { fetchCustomerTypes, fetchAllSubsidiaries } from "@/app/api/customer"; -import CustomerDetail from "./CustomerDetail"; +import CustomerSave from "./CustomerSave"; // type Props = { // params: { @@ -11,7 +11,7 @@ import CustomerDetail from "./CustomerDetail"; // }; // }; -const CustomerDetailWrapper: React.FC = async () => { +const CustomerSaveWrapper: React.FC = async () => { // const { params } = props // console.log(params) const [subsidiaries, customerTypes] = @@ -21,8 +21,8 @@ const CustomerDetailWrapper: React.FC = async () => { ]); return ( - + ); }; -export default CustomerDetailWrapper; +export default CustomerSaveWrapper; diff --git a/src/components/CustomerDetail/SubsidiaryAllocation.tsx b/src/components/CustomerSave/SubsidiaryAllocation.tsx similarity index 100% rename from src/components/CustomerDetail/SubsidiaryAllocation.tsx rename to src/components/CustomerSave/SubsidiaryAllocation.tsx diff --git a/src/components/CustomerSave/index.ts b/src/components/CustomerSave/index.ts new file mode 100644 index 0000000..ea74d25 --- /dev/null +++ b/src/components/CustomerSave/index.ts @@ -0,0 +1 @@ +export { default } from "./CustomerSaveWrapper"; \ No newline at end of file diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx index fc6204d..4111d14 100644 --- a/src/components/StaffSearch/StaffSearch.tsx +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -68,7 +68,7 @@ const StaffSearch: React.FC = ({ staff }) => { const deleteClick = useCallback((staff: StaffResult) => { deleteDialog(async () => { await deleteStaff(staff.id); - successDialog("Delete Success", t); + successDialog(t("Delete Success"), t); setFilteredStaff((prev) => prev.filter((obj) => obj.id !== staff.id)); }, t); }, []); diff --git a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx index 1a9ced0..c335042 100644 --- a/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx +++ b/src/components/SubsidiaryDetail/SubsidiaryDetailWrapper.tsx @@ -1,7 +1,7 @@ import { fetchAllCustomers, fetchSubsidiaryTypes } from "@/app/api/subsidiary"; import SubsidiaryDetail from "./SubsidiaryDetail"; -const CustomerDetailWrapper: React.FC = async () => { +const CustomerSaveWrapper: React.FC = async () => { const [customers, subsidiaryTypes] = await Promise.all([ fetchAllCustomers(), @@ -13,4 +13,4 @@ const CustomerDetailWrapper: React.FC = async () => { ); }; -export default CustomerDetailWrapper; +export default CustomerSaveWrapper; diff --git a/src/components/SubsidiarySearch/SubsidiarySearch.tsx b/src/components/SubsidiarySearch/SubsidiarySearch.tsx index 95c901f..c4e1db5 100644 --- a/src/components/SubsidiarySearch/SubsidiarySearch.tsx +++ b/src/components/SubsidiarySearch/SubsidiarySearch.tsx @@ -46,7 +46,7 @@ const SubsidiarySearch: React.FC = ({ subsidiaries }) => { deleteDialog(async() => { await deleteSubsidiary(subsidiary.id) - successDialog("Delete Success", t) + successDialog(t("Delete Success"), t) setFilteredSubsidiaries((prev) => prev.filter((obj) => obj.id !== subsidiary.id)) }, t) diff --git a/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx b/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx index 72be3c1..7563f53 100644 --- a/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx +++ b/src/components/TaskTemplateSearch/TaskTemplateSearch.tsx @@ -6,6 +6,10 @@ import SearchBox, { Criterion } from "../SearchBox"; import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import EditNote from "@mui/icons-material/EditNote"; +import { useRouter, useSearchParams } from "next/navigation"; +import DeleteIcon from '@mui/icons-material/Delete'; +import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; +import { deleteTaskTemplate } from "@/app/api/tasks/actions"; interface Props { taskTemplates: TaskTemplate[]; @@ -16,6 +20,8 @@ type SearchParamNames = keyof SearchQuery; const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { const { t } = useTranslation("tasks"); + const searchParams = useSearchParams() + const router = useRouter() const [filteredTemplates, setFilteredTemplates] = useState(taskTemplates); const searchCriteria: Criterion[] = useMemo( @@ -30,7 +36,20 @@ const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { }, [taskTemplates]); const onTaskClick = useCallback((taskTemplate: TaskTemplate) => { - console.log(taskTemplate); + const params = new URLSearchParams(searchParams.toString()) + params.set("id", taskTemplate.id.toString()) + router.replace(`/tasks/edit?${params.toString()}`); + }, []); + + const onDeleteClick = useCallback((taskTemplate: TaskTemplate) => { + + deleteDialog(async () => { + await deleteTaskTemplate(taskTemplate.id) + + successDialog(t("Delete Success"), t) + + setFilteredTemplates((prev) => prev.filter((obj) => obj.id !== taskTemplate.id)) + }, t) }, []); const columns = useMemo[]>( @@ -43,6 +62,13 @@ const TaskTemplateSearch: React.FC = ({ taskTemplates }) => { }, { name: "code", label: t("Task Template Code") }, { name: "name", label: t("Task Template Name") }, + { + name: "id", + label: t("Delete"), + onClick: onDeleteClick, + buttonIcon: , + color: "error" + }, ], [onTaskClick, t], ); diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx index a1db872..71ecb79 100644 --- a/src/components/TeamSearch/TeamSearch.tsx +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -56,7 +56,7 @@ const TeamSearch: React.FC = ({ team }) => { deleteDialog(async () => { await deleteTeam(team.id); - successDialog("Delete Success", t); + successDialog(t("Delete Success"), t); setFilteredTeam((prev) => prev.filter((obj) => obj.id !== team.id)); }, t); diff --git a/src/components/TransferList/TransferList.tsx b/src/components/TransferList/TransferList.tsx index 84cd468..232c486 100644 --- a/src/components/TransferList/TransferList.tsx +++ b/src/components/TransferList/TransferList.tsx @@ -109,7 +109,7 @@ const ItemList: React.FC = ({ {label} - {`${checkedItems.length}/${items.length} selected`} + {`${checkedItems.length}/${items.length} ${t("selected")}`} diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx index 095c544..658d25c 100644 --- a/src/components/UserSearch/UserSearch.tsx +++ b/src/components/UserSearch/UserSearch.tsx @@ -45,7 +45,7 @@ const UserSearch: React.FC = ({ users }) => { deleteDialog(async () => { await deleteUser(users.id); - successDialog("Delete Success", t); + successDialog(t("Delete Success"), t); setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id)); }, t); diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 5f1d289..a7d019a 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -17,6 +17,8 @@ "Do you want to delete?": "Do you want to delete", "Delete Success": "Delete Success", + "Details": "Details", + "Delete": "Delete", "Search": "Search", "Search Criteria": "Search Criteria", "Cancel": "Cancel", diff --git a/src/i18n/en/tasks.json b/src/i18n/en/tasks.json new file mode 100644 index 0000000..d70d00a --- /dev/null +++ b/src/i18n/en/tasks.json @@ -0,0 +1,27 @@ +{ + "Task Template": "Task Template", + "Create Task Template": "Create Task Template", + "Edit Task Template": "Edit Task Template", + + "Task Template Code": "Task Template Code", + "Task Template Name": "Task Template Name", + "Task List Setup": "Task List Setup", + "Task Pool": "Task Pool", + "Task List Template": "Task List Template", + + "Task template code is required": "Task template code is required", + "Task template name is required": "Task template name is required", + + "Do you want to submit?": "Do you want to submit?", + "Submit Success": "Submit Success", + "Submit Fail": "Submit Fail", + "Do you want to delete?": "Do you want to delete?", + "Delete Success": "Delete Success", + + "selected": "selected", + "Details": "Details", + "Delete": "Delete", + "Cancel": "Cancel", + "Submit": "Submit", + "Confirm": "Confirm" +} \ No newline at end of file diff --git a/src/i18n/zh/common.json b/src/i18n/zh/common.json index e4642ea..4ff2fcf 100644 --- a/src/i18n/zh/common.json +++ b/src/i18n/zh/common.json @@ -15,6 +15,8 @@ "Do you want to delete?": "你是否確認要刪除?", "Delete Success": "刪除成功", + "Details": "詳情", + "Delete": "刪除", "Search": "搜尋", "Search Criteria": "搜尋條件", "Cancel": "取消", diff --git a/src/i18n/zh/tasks.json b/src/i18n/zh/tasks.json new file mode 100644 index 0000000..16ba727 --- /dev/null +++ b/src/i18n/zh/tasks.json @@ -0,0 +1,27 @@ +{ + "Task Template": "工作範本", + "Create Task Template": "建立工作範本", + "Edit Task Template": "編輯工作範本", + + "Task Template Code": "工作範本編號", + "Task Template Name": "工作範本名稱", + "Task List Setup": "工作名單設置", + "Task Pool": "所有工作", + "Task List Template": "工作名單範本", + + "Task template code is required": "需要工作範本編號", + "Task template name is required": "需要工作範本名稱", + + "Do you want to submit?": "你是否確認要提交?", + "Submit Success": "提交成功", + "Submit Fail": "提交失敗", + "Do you want to delete?": "你是否確認要刪除?", + "Delete Success": "刪除成功", + + "selected": "已選擇", + "Details": "詳情", + "Delete": "刪除", + "Cancel": "取消", + "Submit": "提交", + "Confirm": "確認" +} \ No newline at end of file