diff --git a/src/app/(main)/settings/changepassword/page.tsx b/src/app/(main)/changepassword/page.tsx similarity index 100% rename from src/app/(main)/settings/changepassword/page.tsx rename to src/app/(main)/changepassword/page.tsx diff --git a/src/app/(main)/dashboard/bspp/page.tsx b/src/app/(main)/dashboard/bspp/page.tsx new file mode 100644 index 0000000..a784c18 --- /dev/null +++ b/src/app/(main)/dashboard/bspp/page.tsx @@ -0,0 +1,21 @@ +import { Metadata } from "next"; +import { I18nProvider } from "@/i18n"; +import { Suspense } from "react"; +import Typography from "@mui/material/Typography"; +import Bspp from "@/components/Bspp"; + +export const metadata: Metadata = { + title: "Project Status by Client", +}; + +const CompanyTeamCashFlow: React.FC = () => { + return ( + + + BSPP + + + + ); +}; +export default CompanyTeamCashFlow; diff --git a/src/app/(main)/settings/group/edit/page.tsx b/src/app/(main)/settings/group/edit/page.tsx index 9055358..f1bfcbd 100644 --- a/src/app/(main)/settings/group/edit/page.tsx +++ b/src/app/(main)/settings/group/edit/page.tsx @@ -1,3 +1,4 @@ +import { searchParamsProps } from "@/app/utils/fetchUtil"; import EditPosition from "@/components/EditPosition"; import EditUserGroup from "@/components/EditUserGroup"; import { I18nProvider, getServerI18n } from "@/i18n"; @@ -8,7 +9,9 @@ export const metadata: Metadata = { title: "Edit User Group", }; -const Group: React.FC = async () => { +const Group: React.FC = async ({ + searchParams, +}) => { const { t } = await getServerI18n("group"); // Preload necessary dependencies @@ -17,7 +20,7 @@ const Group: React.FC = async () => { <> {/* {t("Edit User Group")} */} - + ); diff --git a/src/app/api/group/index.ts b/src/app/api/group/index.ts index 9dcee9e..082b0f3 100644 --- a/src/app/api/group/index.ts +++ b/src/app/api/group/index.ts @@ -14,8 +14,21 @@ export interface UserGroupResult { description: string; } +export type IndivUserGroup = { + authIds: number[]; + data: any; + userIds: number[]; +} + export const fetchGroup = cache(async () => { return serverFetchJson(`${BASE_API_URL}/group`, { next: { tags: ["group"] }, }); }); + + +export const fetchIndivGroup = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/group/${id}`, { + next: { tags: ["group"] }, + }); + }); diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index 555adbd..32e9cee 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -46,7 +46,7 @@ export interface ProjectPotentialDelayReportRequest { // - Monthly Work Hours Report export interface MonthlyWorkHoursReportFilter { staff: string[]; - date: any; + date: string; } export interface MonthlyWorkHoursReportRequest { diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts index fca9727..1ee9eb4 100644 --- a/src/app/api/staff/actions.ts +++ b/src/app/api/staff/actions.ts @@ -20,17 +20,17 @@ export interface CreateStaffInputs { companyId: number; salaryId: number; skillSetId?: number[]; - joinDate: string; + joinDate?: string; currentPositionId: number; - joinPositionId: number; + joinPositionId?: number; gradeId?: number; teamId?: number - departmentId: number; + departmentId?: number; phone1: string; phone2?: string; email: string; - emergContactName: string; - emergContactPhone: string; + emergContactName?: string; + emergContactPhone?: string; employType: string; departDate?: string; departReason?: string; diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index b655631..48aaea6 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -7,6 +7,7 @@ import { UserDetail, UserResult } from "."; import { cache } from "react"; export interface UserInputs { + username: string; name: string; email?: string; addAuthIds?: number[]; diff --git a/src/components/AppBar/Profile.tsx b/src/components/AppBar/Profile.tsx index 9fbe8e5..79d116d 100644 --- a/src/components/AppBar/Profile.tsx +++ b/src/components/AppBar/Profile.tsx @@ -63,7 +63,7 @@ const Profile: React.FC = ({ avatarImageSrc, profileName }) => { {profileName} - { router.replace("/settings/changepassword") }}>{t("Change Password")} + { router.replace("/changepassword") }}>{t("Change Password")} {language === "zh" && { onLangClick("en") }}>{t("Change To English Version")}} {language === "en" && { onLangClick("zh") }}>{t("Change To Chinese Version")}} signOut()}>{t("Sign out")} diff --git a/src/components/Bspp/Bspp.tsx b/src/components/Bspp/Bspp.tsx new file mode 100644 index 0000000..40eb30a --- /dev/null +++ b/src/components/Bspp/Bspp.tsx @@ -0,0 +1,404 @@ +"use client"; +import * as React from "react"; +import Grid from "@mui/material/Grid"; +import { Card, CardHeader } from "@mui/material"; +import ReactApexChart from "react-apexcharts"; +import { ApexOptions } from "apexcharts"; +import "../../app/global.css"; +import { Input, Label } from "reactstrap"; +import Select, { components } from "react-select"; +import { DatePicker } from "@mui/x-date-pickers/DatePicker"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import dayjs, { Dayjs } from "dayjs"; +import isBetweenPlugin from "dayjs/plugin/isBetween"; +import { PickersDay, PickersDayProps } from "@mui/x-date-pickers/PickersDay"; +import { styled } from "@mui/material/styles"; + +dayjs.extend(isBetweenPlugin); +interface CustomPickerDayProps extends PickersDayProps { + isSelected: boolean; + isHovered: boolean; +} + +// Style Day +const CustomPickersDay = styled(PickersDay, { + shouldForwardProp: (prop) => prop !== "isSelected" && prop !== "isHovered", +})(({ theme, isSelected, isHovered, day }) => ({ + borderRadius: 0, + ...(isSelected && { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + "&:hover, &:focus": { + backgroundColor: theme.palette.primary.main, + }, + }), + ...(isHovered && { + backgroundColor: theme.palette.primary[theme.palette.mode], + "&:hover, &:focus": { + backgroundColor: theme.palette.primary[theme.palette.mode], + }, + }), + ...(day.day() === 0 && { + borderTopLeftRadius: "50%", + borderBottomLeftRadius: "50%", + }), + ...(day.day() === 6 && { + borderTopRightRadius: "50%", + borderBottomRightRadius: "50%", + }), +})) as React.ComponentType; + +const isInSameWeek = (dayA: Dayjs, dayB: Dayjs | null | undefined) => { + if (dayB == null) { + return false; + } + + return dayA.isSame(dayB, "week"); +}; + +function Day( + props: PickersDayProps & { + selectedDay?: Dayjs | null; + hoveredDay?: Dayjs | null; + }, +) { + const { day, selectedDay, hoveredDay, ...other } = props; + + return ( + + ); +} + + + +// Main function +const Bspp: React.FC = () => { + + // Set Date + const todayDate = new Date(); + const firstDayOfWeek = new Date(); + const lastDayOfWeek = new Date(); + firstDayOfWeek.setDate(todayDate.getDate() - todayDate.getDay() + 1); + lastDayOfWeek.setDate(todayDate.getDate() - todayDate.getDay() + 7); + const [teamTotalManhoursSpentSelect, setTeamTotalManhoursSpentSelect]: any = + React.useState("Weekly"); + const weekDates: any[] = []; + const monthDates: any[] = []; + const currentDate = dayjs(); + const sixMonthsAgo = currentDate.subtract(6, "month"); + for (let i = 0; i < 7; i++) { + const currentDate = new Date(firstDayOfWeek); + currentDate.setDate(firstDayOfWeek.getDate() + i); + const formattedDate = dayjs(currentDate).format("DD MMM (ddd)"); + weekDates.push(formattedDate); + } + for ( + let date = sixMonthsAgo.clone(); + date.isBefore(currentDate, "month"); + date = date.add(1, "month") + ) { + monthDates.push(date.format("MM-YYYY")); + } + monthDates.push(currentDate.format("MM-YYYY")); + const [teamTotalManhoursSpentPeriod, setTeamTotalManhoursSpentPeriod]: any[] = + React.useState(weekDates); + + // Set fake Data + const [ + teamTotalManhoursSpentPlanData, + setTeamTotalManhoursSpentPlanData, + ]: any[] = React.useState([42, 42, 42, 42, 42, 0, 0]); + const [ + teamTotalManhoursSpentActualData, + setTeamTotalManhoursSpentActualData, + ]: any[] = React.useState([45, 42, 60, 42, 58, 0, 0]); + const [hoveredDay, setHoveredDay] = React.useState(null); + const [value, setValue] = React.useState(dayjs()); + const [totalManHoursMonthlyFromValue, setTotalManHoursMonthlyFromValue] = + React.useState(dayjs(new Date()).subtract(6, "month")); + const [totalManHoursMonthlyToValue, setTotalManHoursMonthlyToValue] = + React.useState(dayjs()); + const [totalManHoursMaxValue, setTotalManHoursMaxValue] = React.useState(75); + + const teamOptions = [ + { value: 1, label: "XXX Team" }, + { value: 2, label: "YYY Team" }, + { value: 3, label: "ZZZ Team" }, + ]; + + // https://poe.com/ + // https://apexcharts.com/docs/chart-types/line-chart/ + // chart options + const options: ApexOptions = { + chart: { + height: 350, + type: "line", + }, + stroke: { + width: [2, 2], + }, + plotOptions: { + bar: { + horizontal: false, + distributed: false, + }, + }, + dataLabels: { + enabled: true, + }, + xaxis: { + categories: teamTotalManhoursSpentPeriod, + }, + yaxis: [ + { + title: { + text: "Team Total Manhours Spent (Hour)", + }, + min: 0, + max: totalManHoursMaxValue, + tickAmount: 5, + }, + ], + grid: { + borderColor: "#f1f1f1", + }, + annotations: {}, + series: [ + { + name: "Planned", + type: "line", + color: "#efbe7d", + data: teamTotalManhoursSpentPlanData, + }, + { + name: "Actual", + type: "line", + color: "#7cd3f2", + data: teamTotalManhoursSpentActualData, + }, + ], + }; + + // On click function + const teamTotalManhoursSpentOnClick = (r: any) => { + setTeamTotalManhoursSpentSelect(r); + if (r === "Weekly") { + setValue(dayjs(new Date())); + setTeamTotalManhoursSpentPeriod(weekDates); + setTeamTotalManhoursSpentPlanData([42, 42, 42, 42, 42, 0, 0]); + setTeamTotalManhoursSpentActualData([45, 42, 60, 42, 58, 0, 0]); + setTotalManHoursMaxValue(75); + } else if (r === "Monthly") { + setTeamTotalManhoursSpentPeriod(monthDates); + setTeamTotalManhoursSpentPlanData([840, 840, 840, 840, 840, 840]); + setTeamTotalManhoursSpentActualData([900, 840, 1200, 840, 1160, 840]); + setTotalManHoursMaxValue(1250); + } + }; + + const selectWeeklyPeriod = (r: any) => { + const selectDate = new Date(r); + const firstDayOfWeek = new Date(); + firstDayOfWeek.setDate(selectDate.getDate() - selectDate.getDay() + 0); + const weekDates: any[] = []; + for (let i = 0; i < 7; i++) { + const currentDate = new Date(firstDayOfWeek); + currentDate.setDate(firstDayOfWeek.getDate() + i); + const formattedDate = dayjs(currentDate).format("DD MMM (ddd)"); + weekDates.push(formattedDate); + } + setTeamTotalManhoursSpentPeriod(weekDates); + setValue(dayjs(firstDayOfWeek)); + }; + + const selectMonthlyPeriodFrom = (r: any) => { + const monthDates: any[] = []; + const monthPlanData: any[] = []; + const monthActualData: any[] = []; + const selectFromDate = dayjs(r); + for ( + let date = selectFromDate.clone(); + date.isBefore(totalManHoursMonthlyToValue, "month"); + date = date.add(1, "month") + ) { + monthDates.push(date.format("MM-YYYY")); + monthPlanData.push(840); + monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840)); + } + monthDates.push(totalManHoursMonthlyToValue.format("MM-YYYY")); + monthPlanData.push(840); + monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840)); + setTeamTotalManhoursSpentPlanData(monthPlanData); + setTeamTotalManhoursSpentActualData(monthActualData); + setTeamTotalManhoursSpentPeriod(monthDates); + }; + + const selectMonthlyPeriodTo = (r: any) => { + const monthDates: any[] = []; + const monthPlanData: any[] = []; + const monthActualData: any[] = []; + const selectToDate = dayjs(r); + for ( + let date = totalManHoursMonthlyFromValue.clone(); + date.isBefore(selectToDate, "month"); + date = date.add(1, "month") + ) { + monthDates.push(date.format("MM-YYYY")); + monthPlanData.push(840); + monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840)); + } + monthDates.push(selectToDate.format("MM-YYYY")); + monthPlanData.push(840); + monthActualData.push(Math.floor(Math.random() * (1200 - 840) + 840)); + setTeamTotalManhoursSpentPlanData(monthPlanData); + setTeamTotalManhoursSpentActualData(monthActualData); + setTeamTotalManhoursSpentPeriod(monthDates); + }; + + // render + return ( + <> + +
+
+ + + +
+ {/* Weekly / Monthly Button select */} + {/* Different: selected style & on click function */} +
+ {teamTotalManhoursSpentSelect === "Weekly" && ( + <> + + + + )} + {teamTotalManhoursSpentSelect === "Monthly" && ( + <> + + + + )} +
+ + {/* Team combo */} +
+ + {/* Label */} +
+ +
+ + {/* Select Option */} +
+