| @@ -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 ( | |||
| <I18nProvider namespaces={["dashboard"]}> | |||
| <Typography variant="h4" marginInlineEnd={2}> | |||
| BSPP | |||
| </Typography> | |||
| <Bspp /> | |||
| </I18nProvider> | |||
| ); | |||
| }; | |||
| export default CompanyTeamCashFlow; | |||
| @@ -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<searchParamsProps> = async ({ | |||
| searchParams, | |||
| }) => { | |||
| const { t } = await getServerI18n("group"); | |||
| // Preload necessary dependencies | |||
| @@ -17,7 +20,7 @@ const Group: React.FC = async () => { | |||
| <> | |||
| {/* <Typography variant="h4">{t("Edit User Group")}</Typography> */} | |||
| <I18nProvider namespaces={["group"]}> | |||
| <EditUserGroup /> | |||
| <EditUserGroup id={parseInt(searchParams.id as string)}/> | |||
| </I18nProvider> | |||
| </> | |||
| ); | |||
| @@ -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<Records>(`${BASE_API_URL}/group`, { | |||
| next: { tags: ["group"] }, | |||
| }); | |||
| }); | |||
| export const fetchIndivGroup = cache(async (id: number) => { | |||
| return serverFetchJson<IndivUserGroup>(`${BASE_API_URL}/group/${id}`, { | |||
| next: { tags: ["group"] }, | |||
| }); | |||
| }); | |||
| @@ -46,7 +46,7 @@ export interface ProjectPotentialDelayReportRequest { | |||
| // - Monthly Work Hours Report | |||
| export interface MonthlyWorkHoursReportFilter { | |||
| staff: string[]; | |||
| date: any; | |||
| date: string; | |||
| } | |||
| export interface MonthlyWorkHoursReportRequest { | |||
| @@ -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; | |||
| @@ -7,6 +7,7 @@ import { UserDetail, UserResult } from "."; | |||
| import { cache } from "react"; | |||
| export interface UserInputs { | |||
| username: string; | |||
| name: string; | |||
| email?: string; | |||
| addAuthIds?: number[]; | |||
| @@ -63,7 +63,7 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => { | |||
| {profileName} | |||
| </Typography> | |||
| <Divider /> | |||
| <MenuItem onClick={() => { router.replace("/settings/changepassword") }}>{t("Change Password")}</MenuItem> | |||
| <MenuItem onClick={() => { router.replace("/changepassword") }}>{t("Change Password")}</MenuItem> | |||
| {language === "zh" && <MenuItem onClick={() => { onLangClick("en") }}>{t("Change To English Version")}</MenuItem>} | |||
| {language === "en" && <MenuItem onClick={() => { onLangClick("zh") }}>{t("Change To Chinese Version")}</MenuItem>} | |||
| <MenuItem onClick={() => signOut()}>{t("Sign out")}</MenuItem> | |||
| @@ -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<Dayjs> { | |||
| isSelected: boolean; | |||
| isHovered: boolean; | |||
| } | |||
| // Style Day | |||
| const CustomPickersDay = styled(PickersDay, { | |||
| shouldForwardProp: (prop) => prop !== "isSelected" && prop !== "isHovered", | |||
| })<CustomPickerDayProps>(({ 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<CustomPickerDayProps>; | |||
| const isInSameWeek = (dayA: Dayjs, dayB: Dayjs | null | undefined) => { | |||
| if (dayB == null) { | |||
| return false; | |||
| } | |||
| return dayA.isSame(dayB, "week"); | |||
| }; | |||
| function Day( | |||
| props: PickersDayProps<Dayjs> & { | |||
| selectedDay?: Dayjs | null; | |||
| hoveredDay?: Dayjs | null; | |||
| }, | |||
| ) { | |||
| const { day, selectedDay, hoveredDay, ...other } = props; | |||
| return ( | |||
| <CustomPickersDay | |||
| {...other} | |||
| day={day} | |||
| sx={{ px: 2.5 }} | |||
| disableMargin | |||
| selected={false} | |||
| isSelected={isInSameWeek(day, selectedDay)} | |||
| isHovered={isInSameWeek(day, hoveredDay)} | |||
| /> | |||
| ); | |||
| } | |||
| // 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<Dayjs | null>(null); | |||
| const [value, setValue] = React.useState<Dayjs | null>(dayjs()); | |||
| const [totalManHoursMonthlyFromValue, setTotalManHoursMonthlyFromValue] = | |||
| React.useState<Dayjs>(dayjs(new Date()).subtract(6, "month")); | |||
| const [totalManHoursMonthlyToValue, setTotalManHoursMonthlyToValue] = | |||
| React.useState<Dayjs>(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 ( | |||
| <> | |||
| <Grid item sm> | |||
| <div style={{ display: "inline-block", width: "40%" }}> | |||
| <div> | |||
| <Grid item xs={12} md={12} lg={12}> | |||
| <Card> | |||
| <CardHeader | |||
| className="text-slate-500" | |||
| title="Team Total Manhours Spent" | |||
| /> | |||
| <div style={{ display: "inline-block", width: "99%" }}> | |||
| {/* Weekly / Monthly Button select */} | |||
| {/* Different: selected style & on click function */} | |||
| <div className="w-fit align-top mr-5 float-right"> | |||
| {teamTotalManhoursSpentSelect === "Weekly" && ( | |||
| <> | |||
| <button className="text-lg bg-violet-100 border-violet-500 text-violet-500 border-solid rounded-l-md w-32"> | |||
| Weekly | |||
| </button> | |||
| <button | |||
| onClick={() => | |||
| teamTotalManhoursSpentOnClick("Monthly") | |||
| } | |||
| className="hover:cursor-pointer hover:bg-violet-50 text-lg bg-transparent border-violet-500 text-violet-500 border-solid rounded-r-md w-32" | |||
| > | |||
| Monthly | |||
| </button> | |||
| </> | |||
| )} | |||
| {teamTotalManhoursSpentSelect === "Monthly" && ( | |||
| <> | |||
| <button | |||
| onClick={() => | |||
| teamTotalManhoursSpentOnClick("Weekly") | |||
| } | |||
| className="hover:cursor-pointer hover:bg-violet-50 text-lg bg-transparent border-violet-500 text-violet-500 border-solid rounded-l-md w-32" | |||
| > | |||
| Weekly | |||
| </button> | |||
| <button className="text-lg bg-violet-100 border-violet-500 text-violet-500 border-solid rounded-r-md w-32"> | |||
| Monthly | |||
| </button> | |||
| </> | |||
| )} | |||
| </div> | |||
| {/* Team combo */} | |||
| <div className="inline-block w-fit mt-2"> | |||
| {/* Label */} | |||
| <div className="inline-block ml-6"> | |||
| <Label className="text-slate-500 font-medium"> | |||
| Team: | |||
| </Label> | |||
| </div> | |||
| {/* Select Option */} | |||
| <div className="inline-block ml-1 w-60"> | |||
| <Select | |||
| placeholder="All Team" | |||
| options={teamOptions} | |||
| isClearable={true} | |||
| /> | |||
| </div> | |||
| {/* Weekly / Monthly Period */} | |||
| <div className="ml-6 mt-2" style={{ verticalAlign: "top" }}> | |||
| {teamTotalManhoursSpentSelect === "Weekly" && ( | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| <DatePicker | |||
| className="w-72 h-10 align-top" | |||
| label="Period:" | |||
| value={value} | |||
| format="DD-MM-YYYY" | |||
| onChange={(newValue) => | |||
| selectWeeklyPeriod(newValue) | |||
| } | |||
| showDaysOutsideCurrentMonth | |||
| displayWeekNumber | |||
| slots={{ day: Day }} | |||
| slotProps={{ | |||
| day: (ownerState) => | |||
| ({ | |||
| selectedDay: value, | |||
| hoveredDay, | |||
| onPointerEnter: () => | |||
| setHoveredDay(ownerState.day), | |||
| onPointerLeave: () => setHoveredDay(null), | |||
| }) as any, | |||
| }} | |||
| /> | |||
| </LocalizationProvider> | |||
| )} | |||
| {teamTotalManhoursSpentSelect === "Monthly" && ( | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| <DatePicker | |||
| className="w-40 h-10 align-top" | |||
| onChange={(newValue) => | |||
| selectMonthlyPeriodFrom(newValue) | |||
| } | |||
| defaultValue={totalManHoursMonthlyFromValue} | |||
| label={"Form"} | |||
| views={["month", "year"]} | |||
| /> | |||
| <DatePicker | |||
| className="w-40 h-10 align-top" | |||
| onChange={(newValue) => | |||
| selectMonthlyPeriodTo(newValue) | |||
| } | |||
| defaultValue={totalManHoursMonthlyToValue} | |||
| label={"To"} | |||
| views={["month", "year"]} | |||
| /> | |||
| </LocalizationProvider> | |||
| )} | |||
| </div> | |||
| </div> | |||
| {/* Render Apex Chart */} | |||
| <ReactApexChart | |||
| options={options} | |||
| series={options.series} | |||
| type="line" | |||
| height="400" | |||
| /> | |||
| </div> | |||
| </Card> | |||
| </Grid> | |||
| </div> | |||
| </div> | |||
| </Grid> | |||
| </> | |||
| ); | |||
| }; | |||
| export default Bspp; | |||
| @@ -0,0 +1,10 @@ | |||
| import Bspp from "./Bspp"; | |||
| const BsppWrapper: React.FC = async () => { | |||
| return ( | |||
| <Bspp></Bspp> | |||
| ); | |||
| }; | |||
| export default BsppWrapper; | |||
| @@ -0,0 +1 @@ | |||
| export { default } from "./BsppWrapper"; | |||
| @@ -277,6 +277,7 @@ const CreateProject: React.FC<Props> = ({ | |||
| projectName: mainProjects !== undefined ? mainProjects[0].projectName : undefined, | |||
| projectDescription: mainProjects !== undefined ? mainProjects[0].projectDescription : undefined, | |||
| expectedProjectFee: mainProjects !== undefined ? mainProjects[0].expectedProjectFee : undefined, | |||
| clientId: allCustomers !== undefined ? allCustomers[0].id : undefined, | |||
| ...defaultInputs, | |||
| // manhourPercentageByGrade should have a sensible default | |||
| @@ -430,9 +431,11 @@ const CreateProject: React.FC<Props> = ({ | |||
| startIcon={<Check />} | |||
| type="submit" | |||
| disabled={ | |||
| formProps.getValues("projectDeleted") === true || formProps.getValues("projectStatus") === "Deleted" || | |||
| (!!formProps.getValues("projectActualStart") && | |||
| !!formProps.getValues("projectActualEnd")) | |||
| formProps.getValues("projectDeleted") === true || formProps.getValues("projectStatus").toLowerCase() === "deleted" || | |||
| ( | |||
| // !!formProps.getValues("projectActualStart") && | |||
| !!(formProps.getValues("projectStatus").toLowerCase() === "completed") | |||
| ) | |||
| } | |||
| > | |||
| {isEditMode ? t("Save") : t("Confirm")} | |||
| @@ -118,7 +118,7 @@ const StaffAllocation: React.FC<Props> = ({ | |||
| }, | |||
| { label: t("Team"), name: "team" }, | |||
| { label: t("Grade"), name: "grade" }, | |||
| { label: t("Staff ID"), name: "id" }, | |||
| { label: t("Staff ID"), name: "staffId" }, | |||
| { label: t("Staff Name"), name: "name" }, | |||
| { label: t("Title"), name: "currentPosition" }, | |||
| ], | |||
| @@ -80,7 +80,7 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
| haveError = true | |||
| formProps.setError("phone1", { message: t("Please Enter Correct Phone No.."), type: "required" }) | |||
| } | |||
| if(!regex_phone.test(data.emergContactPhone)) { | |||
| if(data.emergContactPhone && !regex_phone.test(data.emergContactPhone)) { | |||
| haveError = true | |||
| formProps.setError("emergContactPhone", { message: t("Please Enter Correct Phone No.."), type: "required" }) | |||
| } | |||
| @@ -110,19 +110,11 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||
| haveError = true | |||
| formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" }) | |||
| } | |||
| if (!data.departmentId) { | |||
| haveError = true | |||
| formProps.setError("departmentId", { message: t("Please Enter Department."), type: "required" }) | |||
| } | |||
| if (!data.salaryId) { | |||
| haveError = true | |||
| formProps.setError("salaryId", { message: t("Please Enter Salary."), type: "required" }) | |||
| } | |||
| if (!data.joinDate) { | |||
| haveError = true | |||
| formProps.setError("joinDate", { message: t("Please Enter Join Date."), type: "required" }) | |||
| } | |||
| if (data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
| if (data.joinDate &&data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
| haveError = true | |||
| formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | |||
| } | |||
| @@ -165,7 +165,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel required>{t("Department")}</InputLabel> | |||
| <InputLabel>{t("Department")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="departmentId" | |||
| @@ -377,9 +377,11 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| label={t("Emergency Contact Name")} | |||
| fullWidth | |||
| required | |||
| {...register("emergContactName", { | |||
| required: "Emergency Contact Name required!", | |||
| })} | |||
| {...register("emergContactName" | |||
| // , { | |||
| // required: "Emergency Contact Name required!", | |||
| // } | |||
| )} | |||
| error={Boolean(errors.emergContactName)} | |||
| helperText={ | |||
| Boolean(errors.emergContactName) && | |||
| @@ -394,9 +396,11 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| label={t("Emergency Contact Phone")} | |||
| fullWidth | |||
| required | |||
| {...register("emergContactPhone", { | |||
| required: "Emergency Contact Phone required!", | |||
| })} | |||
| {...register("emergContactPhone" | |||
| // , { | |||
| // required: "Emergency Contact Phone required!", | |||
| // } | |||
| )} | |||
| error={Boolean(errors.emergContactPhone)} | |||
| helperText={ | |||
| Boolean(errors.emergContactPhone) && | |||
| @@ -421,7 +425,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| required: true, | |||
| // required: true, | |||
| error: | |||
| joinDate === "Invalid Date" || Boolean(errors.joinDate), | |||
| // value: errors.joinDate?.message, | |||
| @@ -432,7 +436,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <FormControl fullWidth> | |||
| <InputLabel required>{t("Join Position")}</InputLabel> | |||
| <InputLabel>{t("Join Position")}</InputLabel> | |||
| <Controller | |||
| control={control} | |||
| name="joinPositionId" | |||
| @@ -108,7 +108,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos }) => { | |||
| haveError = true | |||
| formProps.setError("phone1", { message: t("Please Enter Correct Phone No.."), type: "required" }) | |||
| } | |||
| if(!regex_phone.test(data.emergContactPhone)) { | |||
| if(data.emergContactPhone && !regex_phone.test(data.emergContactPhone)) { | |||
| haveError = true | |||
| formProps.setError("emergContactPhone", { message: t("Please Enter Correct Phone No.."), type: "required" }) | |||
| } | |||
| @@ -150,7 +150,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos }) => { | |||
| haveError = true | |||
| formProps.setError("joinDate", { message: t("Please Enter Join Date."), type: "required" }) | |||
| } | |||
| if (data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
| if (data.joinDate &&data.departDate && new Date(data.departDate) <= new Date(data.joinDate)) { | |||
| haveError = true | |||
| formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | |||
| } | |||
| @@ -417,7 +417,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| required: true, | |||
| // required: true, | |||
| error: | |||
| joinDate === "Invalid Date" || Boolean(errors.joinDate), | |||
| // value: errors.joinDate?.message, | |||
| @@ -466,7 +466,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| error: departDate | |||
| error: joinDate && departDate | |||
| ? new Date(joinDate) > new Date(departDate) | |||
| : false, | |||
| }, | |||
| @@ -54,7 +54,7 @@ const EditStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||
| const temp = { | |||
| id: parseInt(idString), | |||
| ...data, | |||
| emergContactPhone: data.emergContactPhone.toString(), | |||
| emergContactPhone: data.emergContactPhone ? data.emergContactPhone.toString() : "", | |||
| phone1: data.phone1.toString(), | |||
| phone2: data.phone1.toString(), | |||
| joinDate: formatJoinDate, | |||
| @@ -175,7 +175,7 @@ const EditTeam: React.FC<Props> = async ({ staff, teamInfo }) => { | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<Close />} | |||
| // onClick={handleCancel} | |||
| onClick={() => router.back()} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| @@ -50,6 +50,7 @@ interface Props { | |||
| } | |||
| const EditUser: React.FC<Props> = async ({ user, rules, auths }) => { | |||
| console.log(user) | |||
| const { t } = useTranslation("user"); | |||
| const formProps = useForm<UserInputs>(); | |||
| const searchParams = useSearchParams(); | |||
| @@ -79,7 +80,7 @@ const EditUser: React.FC<Props> = async ({ user, rules, auths }) => { | |||
| console.log(addAuthIds); | |||
| try { | |||
| formProps.reset({ | |||
| name: user.username, | |||
| username: user.username, | |||
| email: user.email, | |||
| addAuthIds: addAuthIds, | |||
| removeAuthIds: [], | |||
| @@ -145,7 +146,8 @@ const EditUser: React.FC<Props> = async ({ user, rules, auths }) => { | |||
| } | |||
| } | |||
| const userData = { | |||
| name: data.name, | |||
| username: data.username, | |||
| name: user.name, | |||
| locked: false, | |||
| addAuthIds: data.addAuthIds || [], | |||
| removeAuthIds: data.removeAuthIds || [], | |||
| @@ -15,14 +15,16 @@ interface SubComponents { | |||
| } | |||
| const EditUserWrapper: React.FC<searchParamsProps> & SubComponents = async ({ | |||
| searchParams | |||
| searchParams, | |||
| }) => { | |||
| const id = parseInt(searchParams.id as string) | |||
| const pwRule = await fetchPwRules() | |||
| const user = await fetchUserDetails(id); | |||
| const auths = await fetchAuth("user", id); | |||
| const id = parseInt(searchParams.id as string); | |||
| const [pwRule, user, auths] = await Promise.all([ | |||
| fetchPwRules(), | |||
| fetchUserDetails(id), | |||
| fetchAuth("user", id), | |||
| ]); | |||
| return <EditUser user={user.data} rules={pwRule} auths={auths.records}/> | |||
| return <EditUser user={user.data} rules={pwRule} auths={auths.records} />; | |||
| }; | |||
| EditUserWrapper.Loading = EditUserLoading; | |||
| @@ -34,10 +34,10 @@ const UserDetail: React.FC = () => { | |||
| <TextField | |||
| label={t("username")} | |||
| fullWidth | |||
| {...register("name", { | |||
| {...register("username", { | |||
| required: "username required!", | |||
| })} | |||
| error={Boolean(errors.name)} | |||
| error={Boolean(errors.username)} | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| @@ -1,6 +1,6 @@ | |||
| "use client"; | |||
| import { useRouter, useSearchParams } from "next/navigation"; | |||
| import { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| // import { TeamResult } from "@/app/api/team"; | |||
| import { useTranslation } from "react-i18next"; | |||
| @@ -13,22 +13,22 @@ import { | |||
| useForm, | |||
| useFormContext, | |||
| } from "react-hook-form"; | |||
| import { Check, Close, Error } from "@mui/icons-material"; | |||
| import { Check, Close, Error, RestartAlt } from "@mui/icons-material"; | |||
| import { StaffResult } from "@/app/api/staff"; | |||
| import { CreateGroupInputs, auth, fetchAuth, saveGroup } from "@/app/api/group/actions"; | |||
| import { UserGroupResult } from "@/app/api/group"; | |||
| import { IndivUserGroup, UserGroupResult } from "@/app/api/group"; | |||
| import { UserResult } from "@/app/api/user"; | |||
| import GroupInfo from "./GroupInfo"; | |||
| import AuthorityAllocation from "./AuthorityAllocation"; | |||
| import UserAllocation from "./UserAllocation"; | |||
| interface Props { | |||
| groups: UserGroupResult[]; | |||
| // auths: auth[]; | |||
| // groups: UserGroupResult[]; | |||
| auths: auth[]; | |||
| users: UserResult[]; | |||
| group: IndivUserGroup | |||
| } | |||
| const EditUserGroup: React.FC<Props> = ({ groups, users }) => { | |||
| // console.log(users) | |||
| const EditUserGroup: React.FC<Props> = ({ users, auths, group }) => { | |||
| const { t } = useTranslation(); | |||
| const [serverError, setServerError] = useState(""); | |||
| const formProps = useForm<CreateGroupInputs>(); | |||
| @@ -36,7 +36,6 @@ const EditUserGroup: React.FC<Props> = ({ groups, users }) => { | |||
| const id = parseInt(searchParams.get("id") || "0"); | |||
| const router = useRouter(); | |||
| const [tabIndex, setTabIndex] = useState(0); | |||
| const [auths, setAuths] = useState<auth[]>(); | |||
| const errors = formProps.formState.errors; | |||
| @@ -79,22 +78,20 @@ const EditUserGroup: React.FC<Props> = ({ groups, users }) => { | |||
| }, | |||
| [router] | |||
| ); | |||
| const resetGroup = React.useCallback(() => { | |||
| formProps.reset({ | |||
| name: group.data.name, | |||
| description: group.data.description, | |||
| addAuthIds: group.authIds, | |||
| addUserIds: group.userIds, | |||
| }) | |||
| }, [group, users]); | |||
| useEffect(() => { | |||
| const thisGroup = groups.filter((item) => item.id === id)[0]; | |||
| const addUserIds = users.filter((item) => item.groupId === id).map((data) => data.id) | |||
| let addAuthIds: number[] = [] | |||
| fetchAuth("group", id).then((data) => { | |||
| setAuths(data.records) | |||
| addAuthIds = data.records.filter((data) => data.v === 1).map((data) => data.id).sort((a, b) => a - b); | |||
| formProps.reset({ | |||
| name: thisGroup.name, | |||
| description: thisGroup.description, | |||
| addAuthIds: addAuthIds, | |||
| addUserIds: addUserIds, | |||
| }); | |||
| }); | |||
| // console.log(auths) | |||
| }, [groups, users]); | |||
| resetGroup() | |||
| }, []); | |||
| // }, [group, users]); | |||
| return ( | |||
| <> | |||
| @@ -140,10 +137,17 @@ const EditUserGroup: React.FC<Props> = ({ groups, users }) => { | |||
| {tabIndex === 1 && <AuthorityAllocation auth={auths!!}/>} | |||
| {tabIndex === 2 && <UserAllocation users={users!!} />} | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| variant="text" | |||
| startIcon={<RestartAlt />} | |||
| onClick={resetGroup} | |||
| > | |||
| {t("Reset")} | |||
| </Button> | |||
| <Button | |||
| variant="outlined" | |||
| startIcon={<Close />} | |||
| // onClick={handleCancel} | |||
| onClick={() => router.back()} | |||
| > | |||
| {t("Cancel")} | |||
| </Button> | |||
| @@ -1,24 +1,31 @@ | |||
| import React from "react"; | |||
| import EditUserGroup from "./EditUserGroup"; | |||
| import EditUserGroupLoading from "./EditUserGroupLoading"; | |||
| import { fetchGroup } from "@/app/api/group"; | |||
| import { fetchGroup, fetchIndivGroup } from "@/app/api/group"; | |||
| import { fetchUser } from "@/app/api/user"; | |||
| import { fetchAuth } from "@/app/api/group/actions"; | |||
| interface SubComponents { | |||
| Loading: typeof EditUserGroupLoading; | |||
| } | |||
| const EditUserGroupWrapper: React.FC & SubComponents = async () => { | |||
| interface Props { | |||
| id: number | |||
| } | |||
| const EditUserGroupWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | |||
| const [ | |||
| groups, | |||
| group, | |||
| users, | |||
| auths, | |||
| ] = await Promise.all([ | |||
| fetchGroup(), | |||
| fetchIndivGroup(id), | |||
| fetchUser(), | |||
| fetchAuth("group", id), | |||
| ]); | |||
| return <EditUserGroup groups={groups.records} users={users}/>; | |||
| return <EditUserGroup users={users} auths={auths.records} group={group}/>; | |||
| }; | |||
| EditUserGroupWrapper.Loading = EditUserGroupLoading; | |||
| @@ -43,6 +43,7 @@ const GroupInfo: React.FC = () => { | |||
| <TextField | |||
| label={t("Group Name")} | |||
| fullWidth | |||
| rows={4} | |||
| {...register("name", { | |||
| required: true, | |||
| })} | |||
| @@ -49,11 +49,17 @@ const GenerateMonthlyWorkHoursReport: React.FC<Props> = ({ staffs }) => { | |||
| formType={"download"} | |||
| criteria={searchCriteria} | |||
| onSearch={async (query: any) => { | |||
| console.log(query); | |||
| const index = staffCombo.findIndex((staff) => staff === query.staff); | |||
| const response = await fetchMonthlyWorkHoursReport({ | |||
| let postData = { | |||
| id: staffs[index].id, | |||
| yearMonth: query.date, | |||
| }); | |||
| yearMonth: dayjs().format("YYYY-MM").toString(), | |||
| }; | |||
| console.log(query.date.length > 0) | |||
| if (query.date.length > 0) { | |||
| postData.yearMonth = query.date | |||
| } | |||
| const response = await fetchMonthlyWorkHoursReport(postData); | |||
| if (response) { | |||
| downloadFile( | |||
| new Uint8Array(response.blobValue), | |||
| @@ -63,7 +63,7 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => { | |||
| { | |||
| icon: <WorkHistory />, | |||
| label: "User Workspace", | |||
| path: "/home", | |||
| path: "/home", | |||
| showOnMobile: true, | |||
| }, | |||
| { | |||
| @@ -71,6 +71,11 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => { | |||
| label: "Dashboard", | |||
| path: "", | |||
| children: [ | |||
| { | |||
| icon: <WorkHistory />, | |||
| label: "BSPP", | |||
| path: "/dashboard/bspp", | |||
| }, | |||
| { | |||
| icon: <SummarizeIcon />, | |||
| label: "Financial Summary", | |||