@@ -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", | |||