@@ -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 EditPosition from "@/components/EditPosition"; | ||||
import EditUserGroup from "@/components/EditUserGroup"; | import EditUserGroup from "@/components/EditUserGroup"; | ||||
import { I18nProvider, getServerI18n } from "@/i18n"; | import { I18nProvider, getServerI18n } from "@/i18n"; | ||||
@@ -8,7 +9,9 @@ export const metadata: Metadata = { | |||||
title: "Edit User Group", | title: "Edit User Group", | ||||
}; | }; | ||||
const Group: React.FC = async () => { | |||||
const Group: React.FC<searchParamsProps> = async ({ | |||||
searchParams, | |||||
}) => { | |||||
const { t } = await getServerI18n("group"); | const { t } = await getServerI18n("group"); | ||||
// Preload necessary dependencies | // Preload necessary dependencies | ||||
@@ -17,7 +20,7 @@ const Group: React.FC = async () => { | |||||
<> | <> | ||||
{/* <Typography variant="h4">{t("Edit User Group")}</Typography> */} | {/* <Typography variant="h4">{t("Edit User Group")}</Typography> */} | ||||
<I18nProvider namespaces={["group"]}> | <I18nProvider namespaces={["group"]}> | ||||
<EditUserGroup /> | |||||
<EditUserGroup id={parseInt(searchParams.id as string)}/> | |||||
</I18nProvider> | </I18nProvider> | ||||
</> | </> | ||||
); | ); | ||||
@@ -14,8 +14,21 @@ export interface UserGroupResult { | |||||
description: string; | description: string; | ||||
} | } | ||||
export type IndivUserGroup = { | |||||
authIds: number[]; | |||||
data: any; | |||||
userIds: number[]; | |||||
} | |||||
export const fetchGroup = cache(async () => { | export const fetchGroup = cache(async () => { | ||||
return serverFetchJson<Records>(`${BASE_API_URL}/group`, { | return serverFetchJson<Records>(`${BASE_API_URL}/group`, { | ||||
next: { tags: ["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 | // - Monthly Work Hours Report | ||||
export interface MonthlyWorkHoursReportFilter { | export interface MonthlyWorkHoursReportFilter { | ||||
staff: string[]; | staff: string[]; | ||||
date: any; | |||||
date: string; | |||||
} | } | ||||
export interface MonthlyWorkHoursReportRequest { | export interface MonthlyWorkHoursReportRequest { | ||||
@@ -20,17 +20,17 @@ export interface CreateStaffInputs { | |||||
companyId: number; | companyId: number; | ||||
salaryId: number; | salaryId: number; | ||||
skillSetId?: number[]; | skillSetId?: number[]; | ||||
joinDate: string; | |||||
joinDate?: string; | |||||
currentPositionId: number; | currentPositionId: number; | ||||
joinPositionId: number; | |||||
joinPositionId?: number; | |||||
gradeId?: number; | gradeId?: number; | ||||
teamId?: number | teamId?: number | ||||
departmentId: number; | |||||
departmentId?: number; | |||||
phone1: string; | phone1: string; | ||||
phone2?: string; | phone2?: string; | ||||
email: string; | email: string; | ||||
emergContactName: string; | |||||
emergContactPhone: string; | |||||
emergContactName?: string; | |||||
emergContactPhone?: string; | |||||
employType: string; | employType: string; | ||||
departDate?: string; | departDate?: string; | ||||
departReason?: string; | departReason?: string; | ||||
@@ -7,6 +7,7 @@ import { UserDetail, UserResult } from "."; | |||||
import { cache } from "react"; | import { cache } from "react"; | ||||
export interface UserInputs { | export interface UserInputs { | ||||
username: string; | |||||
name: string; | name: string; | ||||
email?: string; | email?: string; | ||||
addAuthIds?: number[]; | addAuthIds?: number[]; | ||||
@@ -63,7 +63,7 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => { | |||||
{profileName} | {profileName} | ||||
</Typography> | </Typography> | ||||
<Divider /> | <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 === "zh" && <MenuItem onClick={() => { onLangClick("en") }}>{t("Change To English Version")}</MenuItem>} | ||||
{language === "en" && <MenuItem onClick={() => { onLangClick("zh") }}>{t("Change To Chinese Version")}</MenuItem>} | {language === "en" && <MenuItem onClick={() => { onLangClick("zh") }}>{t("Change To Chinese Version")}</MenuItem>} | ||||
<MenuItem onClick={() => signOut()}>{t("Sign out")}</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, | projectName: mainProjects !== undefined ? mainProjects[0].projectName : undefined, | ||||
projectDescription: mainProjects !== undefined ? mainProjects[0].projectDescription : undefined, | projectDescription: mainProjects !== undefined ? mainProjects[0].projectDescription : undefined, | ||||
expectedProjectFee: mainProjects !== undefined ? mainProjects[0].expectedProjectFee : undefined, | expectedProjectFee: mainProjects !== undefined ? mainProjects[0].expectedProjectFee : undefined, | ||||
clientId: allCustomers !== undefined ? allCustomers[0].id : undefined, | |||||
...defaultInputs, | ...defaultInputs, | ||||
// manhourPercentageByGrade should have a sensible default | // manhourPercentageByGrade should have a sensible default | ||||
@@ -430,9 +431,11 @@ const CreateProject: React.FC<Props> = ({ | |||||
startIcon={<Check />} | startIcon={<Check />} | ||||
type="submit" | type="submit" | ||||
disabled={ | 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")} | {isEditMode ? t("Save") : t("Confirm")} | ||||
@@ -118,7 +118,7 @@ const StaffAllocation: React.FC<Props> = ({ | |||||
}, | }, | ||||
{ label: t("Team"), name: "team" }, | { label: t("Team"), name: "team" }, | ||||
{ label: t("Grade"), name: "grade" }, | { 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("Staff Name"), name: "name" }, | ||||
{ label: t("Title"), name: "currentPosition" }, | { label: t("Title"), name: "currentPosition" }, | ||||
], | ], | ||||
@@ -80,7 +80,7 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||||
haveError = true | haveError = true | ||||
formProps.setError("phone1", { message: t("Please Enter Correct Phone No.."), type: "required" }) | 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 | haveError = true | ||||
formProps.setError("emergContactPhone", { message: t("Please Enter Correct Phone No.."), type: "required" }) | formProps.setError("emergContactPhone", { message: t("Please Enter Correct Phone No.."), type: "required" }) | ||||
} | } | ||||
@@ -110,19 +110,11 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => { | |||||
haveError = true | haveError = true | ||||
formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" }) | 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) { | if (!data.salaryId) { | ||||
haveError = true | haveError = true | ||||
formProps.setError("salaryId", { message: t("Please Enter Salary."), type: "required" }) | 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 | haveError = true | ||||
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | 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> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<FormControl fullWidth> | <FormControl fullWidth> | ||||
<InputLabel required>{t("Department")}</InputLabel> | |||||
<InputLabel>{t("Department")}</InputLabel> | |||||
<Controller | <Controller | ||||
control={control} | control={control} | ||||
name="departmentId" | name="departmentId" | ||||
@@ -377,9 +377,11 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
label={t("Emergency Contact Name")} | label={t("Emergency Contact Name")} | ||||
fullWidth | fullWidth | ||||
required | required | ||||
{...register("emergContactName", { | |||||
required: "Emergency Contact Name required!", | |||||
})} | |||||
{...register("emergContactName" | |||||
// , { | |||||
// required: "Emergency Contact Name required!", | |||||
// } | |||||
)} | |||||
error={Boolean(errors.emergContactName)} | error={Boolean(errors.emergContactName)} | ||||
helperText={ | helperText={ | ||||
Boolean(errors.emergContactName) && | Boolean(errors.emergContactName) && | ||||
@@ -394,9 +396,11 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
label={t("Emergency Contact Phone")} | label={t("Emergency Contact Phone")} | ||||
fullWidth | fullWidth | ||||
required | required | ||||
{...register("emergContactPhone", { | |||||
required: "Emergency Contact Phone required!", | |||||
})} | |||||
{...register("emergContactPhone" | |||||
// , { | |||||
// required: "Emergency Contact Phone required!", | |||||
// } | |||||
)} | |||||
error={Boolean(errors.emergContactPhone)} | error={Boolean(errors.emergContactPhone)} | ||||
helperText={ | helperText={ | ||||
Boolean(errors.emergContactPhone) && | Boolean(errors.emergContactPhone) && | ||||
@@ -421,7 +425,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
}} | }} | ||||
slotProps={{ | slotProps={{ | ||||
textField: { | textField: { | ||||
required: true, | |||||
// required: true, | |||||
error: | error: | ||||
joinDate === "Invalid Date" || Boolean(errors.joinDate), | joinDate === "Invalid Date" || Boolean(errors.joinDate), | ||||
// value: errors.joinDate?.message, | // value: errors.joinDate?.message, | ||||
@@ -432,7 +436,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<FormControl fullWidth> | <FormControl fullWidth> | ||||
<InputLabel required>{t("Join Position")}</InputLabel> | |||||
<InputLabel>{t("Join Position")}</InputLabel> | |||||
<Controller | <Controller | ||||
control={control} | control={control} | ||||
name="joinPositionId" | name="joinPositionId" | ||||
@@ -108,7 +108,7 @@ const EditStaff: React.FC<formProps> = ({ Staff, combos }) => { | |||||
haveError = true | haveError = true | ||||
formProps.setError("phone1", { message: t("Please Enter Correct Phone No.."), type: "required" }) | 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 | haveError = true | ||||
formProps.setError("emergContactPhone", { message: t("Please Enter Correct Phone No.."), type: "required" }) | 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 | haveError = true | ||||
formProps.setError("joinDate", { message: t("Please Enter Join Date."), type: "required" }) | 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 | haveError = true | ||||
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" }) | 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={{ | slotProps={{ | ||||
textField: { | textField: { | ||||
required: true, | |||||
// required: true, | |||||
error: | error: | ||||
joinDate === "Invalid Date" || Boolean(errors.joinDate), | joinDate === "Invalid Date" || Boolean(errors.joinDate), | ||||
// value: errors.joinDate?.message, | // value: errors.joinDate?.message, | ||||
@@ -466,7 +466,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => { | |||||
}} | }} | ||||
slotProps={{ | slotProps={{ | ||||
textField: { | textField: { | ||||
error: departDate | |||||
error: joinDate && departDate | |||||
? new Date(joinDate) > new Date(departDate) | ? new Date(joinDate) > new Date(departDate) | ||||
: false, | : false, | ||||
}, | }, | ||||
@@ -54,7 +54,7 @@ const EditStaffForm: React.FC<formProps> = ({ Title, fieldLists }) => { | |||||
const temp = { | const temp = { | ||||
id: parseInt(idString), | id: parseInt(idString), | ||||
...data, | ...data, | ||||
emergContactPhone: data.emergContactPhone.toString(), | |||||
emergContactPhone: data.emergContactPhone ? data.emergContactPhone.toString() : "", | |||||
phone1: data.phone1.toString(), | phone1: data.phone1.toString(), | ||||
phone2: data.phone1.toString(), | phone2: data.phone1.toString(), | ||||
joinDate: formatJoinDate, | joinDate: formatJoinDate, | ||||
@@ -175,7 +175,7 @@ const EditTeam: React.FC<Props> = async ({ staff, teamInfo }) => { | |||||
<Button | <Button | ||||
variant="outlined" | variant="outlined" | ||||
startIcon={<Close />} | startIcon={<Close />} | ||||
// onClick={handleCancel} | |||||
onClick={() => router.back()} | |||||
> | > | ||||
{t("Cancel")} | {t("Cancel")} | ||||
</Button> | </Button> | ||||
@@ -50,6 +50,7 @@ interface Props { | |||||
} | } | ||||
const EditUser: React.FC<Props> = async ({ user, rules, auths }) => { | const EditUser: React.FC<Props> = async ({ user, rules, auths }) => { | ||||
console.log(user) | |||||
const { t } = useTranslation("user"); | const { t } = useTranslation("user"); | ||||
const formProps = useForm<UserInputs>(); | const formProps = useForm<UserInputs>(); | ||||
const searchParams = useSearchParams(); | const searchParams = useSearchParams(); | ||||
@@ -79,7 +80,7 @@ const EditUser: React.FC<Props> = async ({ user, rules, auths }) => { | |||||
console.log(addAuthIds); | console.log(addAuthIds); | ||||
try { | try { | ||||
formProps.reset({ | formProps.reset({ | ||||
name: user.username, | |||||
username: user.username, | |||||
email: user.email, | email: user.email, | ||||
addAuthIds: addAuthIds, | addAuthIds: addAuthIds, | ||||
removeAuthIds: [], | removeAuthIds: [], | ||||
@@ -145,7 +146,8 @@ const EditUser: React.FC<Props> = async ({ user, rules, auths }) => { | |||||
} | } | ||||
} | } | ||||
const userData = { | const userData = { | ||||
name: data.name, | |||||
username: data.username, | |||||
name: user.name, | |||||
locked: false, | locked: false, | ||||
addAuthIds: data.addAuthIds || [], | addAuthIds: data.addAuthIds || [], | ||||
removeAuthIds: data.removeAuthIds || [], | removeAuthIds: data.removeAuthIds || [], | ||||
@@ -15,14 +15,16 @@ interface SubComponents { | |||||
} | } | ||||
const EditUserWrapper: React.FC<searchParamsProps> & SubComponents = async ({ | 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; | EditUserWrapper.Loading = EditUserLoading; | ||||
@@ -34,10 +34,10 @@ const UserDetail: React.FC = () => { | |||||
<TextField | <TextField | ||||
label={t("username")} | label={t("username")} | ||||
fullWidth | fullWidth | ||||
{...register("name", { | |||||
{...register("username", { | |||||
required: "username required!", | required: "username required!", | ||||
})} | })} | ||||
error={Boolean(errors.name)} | |||||
error={Boolean(errors.username)} | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
@@ -1,6 +1,6 @@ | |||||
"use client"; | "use client"; | ||||
import { useRouter, useSearchParams } from "next/navigation"; | 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 SearchResults, { Column } from "../SearchResults"; | ||||
// import { TeamResult } from "@/app/api/team"; | // import { TeamResult } from "@/app/api/team"; | ||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
@@ -13,22 +13,22 @@ import { | |||||
useForm, | useForm, | ||||
useFormContext, | useFormContext, | ||||
} from "react-hook-form"; | } 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 { StaffResult } from "@/app/api/staff"; | ||||
import { CreateGroupInputs, auth, fetchAuth, saveGroup } from "@/app/api/group/actions"; | 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 { UserResult } from "@/app/api/user"; | ||||
import GroupInfo from "./GroupInfo"; | import GroupInfo from "./GroupInfo"; | ||||
import AuthorityAllocation from "./AuthorityAllocation"; | import AuthorityAllocation from "./AuthorityAllocation"; | ||||
import UserAllocation from "./UserAllocation"; | import UserAllocation from "./UserAllocation"; | ||||
interface Props { | interface Props { | ||||
groups: UserGroupResult[]; | |||||
// auths: auth[]; | |||||
// groups: UserGroupResult[]; | |||||
auths: auth[]; | |||||
users: UserResult[]; | 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 { t } = useTranslation(); | ||||
const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
const formProps = useForm<CreateGroupInputs>(); | const formProps = useForm<CreateGroupInputs>(); | ||||
@@ -36,7 +36,6 @@ const EditUserGroup: React.FC<Props> = ({ groups, users }) => { | |||||
const id = parseInt(searchParams.get("id") || "0"); | const id = parseInt(searchParams.get("id") || "0"); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
const [tabIndex, setTabIndex] = useState(0); | const [tabIndex, setTabIndex] = useState(0); | ||||
const [auths, setAuths] = useState<auth[]>(); | |||||
const errors = formProps.formState.errors; | const errors = formProps.formState.errors; | ||||
@@ -79,22 +78,20 @@ const EditUserGroup: React.FC<Props> = ({ groups, users }) => { | |||||
}, | }, | ||||
[router] | [router] | ||||
); | ); | ||||
const resetGroup = React.useCallback(() => { | |||||
formProps.reset({ | |||||
name: group.data.name, | |||||
description: group.data.description, | |||||
addAuthIds: group.authIds, | |||||
addUserIds: group.userIds, | |||||
}) | |||||
}, [group, users]); | |||||
useEffect(() => { | 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 ( | return ( | ||||
<> | <> | ||||
@@ -140,10 +137,17 @@ const EditUserGroup: React.FC<Props> = ({ groups, users }) => { | |||||
{tabIndex === 1 && <AuthorityAllocation auth={auths!!}/>} | {tabIndex === 1 && <AuthorityAllocation auth={auths!!}/>} | ||||
{tabIndex === 2 && <UserAllocation users={users!!} />} | {tabIndex === 2 && <UserAllocation users={users!!} />} | ||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
<Button | |||||
variant="text" | |||||
startIcon={<RestartAlt />} | |||||
onClick={resetGroup} | |||||
> | |||||
{t("Reset")} | |||||
</Button> | |||||
<Button | <Button | ||||
variant="outlined" | variant="outlined" | ||||
startIcon={<Close />} | startIcon={<Close />} | ||||
// onClick={handleCancel} | |||||
onClick={() => router.back()} | |||||
> | > | ||||
{t("Cancel")} | {t("Cancel")} | ||||
</Button> | </Button> | ||||
@@ -1,24 +1,31 @@ | |||||
import React from "react"; | import React from "react"; | ||||
import EditUserGroup from "./EditUserGroup"; | import EditUserGroup from "./EditUserGroup"; | ||||
import EditUserGroupLoading from "./EditUserGroupLoading"; | import EditUserGroupLoading from "./EditUserGroupLoading"; | ||||
import { fetchGroup } from "@/app/api/group"; | |||||
import { fetchGroup, fetchIndivGroup } from "@/app/api/group"; | |||||
import { fetchUser } from "@/app/api/user"; | import { fetchUser } from "@/app/api/user"; | ||||
import { fetchAuth } from "@/app/api/group/actions"; | |||||
interface SubComponents { | interface SubComponents { | ||||
Loading: typeof EditUserGroupLoading; | Loading: typeof EditUserGroupLoading; | ||||
} | } | ||||
const EditUserGroupWrapper: React.FC & SubComponents = async () => { | |||||
interface Props { | |||||
id: number | |||||
} | |||||
const EditUserGroupWrapper: React.FC<Props> & SubComponents = async ({ id }) => { | |||||
const [ | const [ | ||||
groups, | |||||
group, | |||||
users, | users, | ||||
auths, | |||||
] = await Promise.all([ | ] = await Promise.all([ | ||||
fetchGroup(), | |||||
fetchIndivGroup(id), | |||||
fetchUser(), | fetchUser(), | ||||
fetchAuth("group", id), | |||||
]); | ]); | ||||
return <EditUserGroup groups={groups.records} users={users}/>; | |||||
return <EditUserGroup users={users} auths={auths.records} group={group}/>; | |||||
}; | }; | ||||
EditUserGroupWrapper.Loading = EditUserGroupLoading; | EditUserGroupWrapper.Loading = EditUserGroupLoading; | ||||
@@ -43,6 +43,7 @@ const GroupInfo: React.FC = () => { | |||||
<TextField | <TextField | ||||
label={t("Group Name")} | label={t("Group Name")} | ||||
fullWidth | fullWidth | ||||
rows={4} | |||||
{...register("name", { | {...register("name", { | ||||
required: true, | required: true, | ||||
})} | })} | ||||
@@ -49,11 +49,17 @@ const GenerateMonthlyWorkHoursReport: React.FC<Props> = ({ staffs }) => { | |||||
formType={"download"} | formType={"download"} | ||||
criteria={searchCriteria} | criteria={searchCriteria} | ||||
onSearch={async (query: any) => { | onSearch={async (query: any) => { | ||||
console.log(query); | |||||
const index = staffCombo.findIndex((staff) => staff === query.staff); | const index = staffCombo.findIndex((staff) => staff === query.staff); | ||||
const response = await fetchMonthlyWorkHoursReport({ | |||||
let postData = { | |||||
id: staffs[index].id, | 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) { | if (response) { | ||||
downloadFile( | downloadFile( | ||||
new Uint8Array(response.blobValue), | new Uint8Array(response.blobValue), | ||||
@@ -63,7 +63,7 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => { | |||||
{ | { | ||||
icon: <WorkHistory />, | icon: <WorkHistory />, | ||||
label: "User Workspace", | label: "User Workspace", | ||||
path: "/home", | |||||
path: "/home", | |||||
showOnMobile: true, | showOnMobile: true, | ||||
}, | }, | ||||
{ | { | ||||
@@ -71,6 +71,11 @@ const NavigationContent: React.FC<Props> = ({ abilities }) => { | |||||
label: "Dashboard", | label: "Dashboard", | ||||
path: "", | path: "", | ||||
children: [ | children: [ | ||||
{ | |||||
icon: <WorkHistory />, | |||||
label: "BSPP", | |||||
path: "/dashboard/bspp", | |||||
}, | |||||
{ | { | ||||
icon: <SummarizeIcon />, | icon: <SummarizeIcon />, | ||||
label: "Financial Summary", | label: "Financial Summary", | ||||