Bladeren bron

Merge branch 'main' of https://git.2fi-solutions.com/wayne.lee/tsms

tags/Baseline_30082024_FRONTEND_UAT
MSI\2Fi 1 jaar geleden
bovenliggende
commit
c717367332
60 gewijzigde bestanden met toevoegingen van 1368 en 817 verwijderingen
  1. +7
    -4
      src/app/(main)/layout.tsx
  2. +1
    -5
      src/app/(main)/settings/skill/create/page.tsx
  3. +2
    -2
      src/app/(main)/settings/skill/edit/page.tsx
  4. +1
    -1
      src/app/(main)/settings/skill/page.tsx
  5. +3
    -25
      src/app/(main)/settings/team/create/page.tsx
  6. +2
    -2
      src/app/(main)/settings/team/page.tsx
  7. +3
    -0
      src/app/api/customer/index.ts
  8. +10
    -6
      src/app/api/group/actions.ts
  9. +6
    -1
      src/app/api/projects/actions.ts
  10. +1
    -1
      src/app/api/projects/index.ts
  11. +14
    -1
      src/app/api/reports/actions.ts
  12. +11
    -0
      src/app/api/reports/index.ts
  13. +19
    -6
      src/app/api/skill/actions.ts
  14. +4
    -3
      src/app/api/skill/index.ts
  15. +23
    -10
      src/app/api/staff/actions.ts
  16. +1
    -0
      src/app/api/staff/index.ts
  17. +6
    -2
      src/app/api/subsidiary/index.ts
  18. +11
    -6
      src/app/api/team/actions.ts
  19. +13
    -9
      src/app/api/user/actions.ts
  20. +8
    -3
      src/app/utils/fetchUtil.ts
  21. +65
    -24
      src/app/utils/formatUtil.ts
  22. +14
    -3
      src/components/AppBar/Profile.tsx
  23. +36
    -35
      src/components/Breadcrumb/Breadcrumb.tsx
  24. +26
    -25
      src/components/Breadcrumb/Clock.tsx
  25. +0
    -1
      src/components/ChangePassword/ChangePassword.tsx
  26. +15
    -8
      src/components/CreateProject/CreateProject.tsx
  27. +31
    -23
      src/components/CreateProject/MilestoneSection.tsx
  28. +117
    -66
      src/components/CreateProject/ProjectClientDetails.tsx
  29. +32
    -10
      src/components/CreateProject/TaskSetup.tsx
  30. +33
    -26
      src/components/CreateSkill/CreateSkill.tsx
  31. +56
    -48
      src/components/CreateSkill/SkillInfo.tsx
  32. +20
    -10
      src/components/CreateStaff/CreateStaff.tsx
  33. +19
    -18
      src/components/CreateStaff/StaffInfo.tsx
  34. +3
    -6
      src/components/CreateTeam/CreateTeam.tsx
  35. +1
    -4
      src/components/CreateTeam/StaffAllocation.tsx
  36. +41
    -38
      src/components/EditSkill/EditSkill.tsx
  37. +26
    -42
      src/components/EditSkill/EditSkillForm.tsx
  38. +6
    -9
      src/components/EditTeam/Allocation.tsx
  39. +6
    -8
      src/components/EditTeam/EditTeam.tsx
  40. +106
    -0
      src/components/PastEntryCalendar/PastEntryCalendar.tsx
  41. +99
    -0
      src/components/PastEntryCalendar/PastEntryCalendarModal.tsx
  42. +1
    -0
      src/components/PastEntryCalendar/index.ts
  43. +9
    -1
      src/components/ProjectSearch/ProjectSearch.tsx
  44. +111
    -89
      src/components/Report/ReportSearchBox/SearchBox.tsx
  45. +12
    -11
      src/components/SkillSearch/SkillSearch.tsx
  46. +0
    -1
      src/components/SkillSearch/SkillSearchWrapper.tsx
  47. +4
    -4
      src/components/StaffSearch/StaffSearch.tsx
  48. +4
    -0
      src/components/StyledDataGrid/StyledDataGrid.tsx
  49. +16
    -12
      src/components/TeamSearch/TeamSearch.tsx
  50. +7
    -84
      src/components/TimesheetTable/MobileTimesheetEntry.tsx
  51. +135
    -94
      src/components/TimesheetTable/ProjectSelect.tsx
  52. +87
    -0
      src/components/TimesheetTable/TimeEntryCard.tsx
  53. +0
    -3
      src/components/UserGroupSearch/UserGroupSearch.tsx
  54. +0
    -3
      src/components/UserSearch/UserSearch.tsx
  55. +3
    -16
      src/components/UserWorkspacePage/ProjectGrid.tsx
  56. +27
    -8
      src/components/UserWorkspacePage/UserWorkspacePage.tsx
  57. +18
    -0
      src/i18n/zh/skill.json
  58. +1
    -0
      src/i18n/zh/staff.json
  59. +25
    -0
      src/i18n/zh/team.json
  60. +10
    -0
      src/theme/devias-material-kit/components.ts

+ 7
- 4
src/app/(main)/layout.tsx Bestand weergeven

@@ -6,6 +6,7 @@ import Box from "@mui/material/Box";
import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig";
import Stack from "@mui/material/Stack";
import Breadcrumb from "@/components/Breadcrumb";
import { I18nProvider } from "@/i18n";

export default async function MainLayout({
children,
@@ -31,10 +32,12 @@ export default async function MainLayout({
padding: { xs: "1rem", sm: "1.5rem", lg: "3rem" },
}}
>
<Stack spacing={2}>
<Breadcrumb />
{children}
</Stack>
<I18nProvider namespaces={["common"]}>
<Stack spacing={2}>
<Breadcrumb />
{children}
</Stack>
</I18nProvider>
</Box>
</>
);


+ 1
- 5
src/app/(main)/settings/skill/create/page.tsx Bestand weergeven

@@ -28,11 +28,7 @@ import CreateSkill from "@/components/CreateSkill";
// const Title = ["title1", "title2"];

const CreateStaff: React.FC = async () => {
const { t } = await getServerI18n("staff");

const title = ['', t('Additional Info')]
// const regex = new RegExp("^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$")
// console.log(regex)
const { t } = await getServerI18n("skill");

return (
<>


+ 2
- 2
src/app/(main)/settings/skill/edit/page.tsx Bestand weergeven

@@ -17,13 +17,13 @@ const EditSkillPage: React.FC<searchParamsProps> = async ({
searchParams,
}) => {
console.log(searchParams.id)
const { t } = await getServerI18n("staff");
const { t } = await getServerI18n("skill");

return (

<>
<Typography variant="h4">{t("Edit Skill")}</Typography>
<I18nProvider namespaces={["team", "common"]}>
<I18nProvider namespaces={["skill", "common"]}>
<Suspense fallback={<EditSkill.Loading />}>
<EditSkill id={parseInt(searchParams.id as string)}/>
</Suspense>


+ 1
- 1
src/app/(main)/settings/skill/page.tsx Bestand weergeven

@@ -38,7 +38,7 @@ const Skill: React.FC = async () => {
{t("Create Skill")}
</Button>
</Stack>
<I18nProvider namespaces={["staff", "common"]}>
<I18nProvider namespaces={["skill", "common"]}>
<Suspense fallback={<SkillSearch.Loading />}>
<SkillSearch />
</Suspense>


+ 3
- 25
src/app/(main)/settings/team/create/page.tsx Bestand weergeven

@@ -1,28 +1,6 @@
// 'use client';
import { I18nProvider, getServerI18n } from "@/i18n";
import CustomInputForm from "@/components/CustomInputForm";
import Check from "@mui/icons-material/Check";
import Close from "@mui/icons-material/Close";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import Tab from "@mui/material/Tab";
import Tabs, { TabsProps } from "@mui/material/Tabs";
import { useRouter } from "next/navigation";
import React, { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { Task, TaskTemplate } from "@/app/api/tasks";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
} from "react-hook-form";
import { CreateProjectInputs, saveProject } from "@/app/api/projects/actions";
import { Error } from "@mui/icons-material";
import { ProjectCategory } from "@/app/api/projects";
import { Grid, Typography } from "@mui/material";
import CreateStaffForm from "@/components/CreateStaff/CreateStaff";
import React from "react";
import { Typography } from "@mui/material";
import CreateTeam from "@/components/CreateTeam";

const CreateTeamPage: React.FC = async () => {
@@ -31,7 +9,7 @@ const CreateTeamPage: React.FC = async () => {
return (
<>
<Typography variant="h4">{t("Create Team")}</Typography>
<I18nProvider namespaces={["Team"]}>
<I18nProvider namespaces={["team"]}>
<CreateTeam/>
</I18nProvider>
</>


+ 2
- 2
src/app/(main)/settings/team/page.tsx Bestand weergeven

@@ -18,7 +18,7 @@ export const metadata: Metadata = {


const Team: React.FC = async () => {
const { t } = await getServerI18n("Team");
const { t } = await getServerI18n("team");
// preloadTeamLeads();
// preloadStaff();
return (
@@ -41,7 +41,7 @@ export const metadata: Metadata = {
{t("Create Team")}
</Button>
</Stack>
<I18nProvider namespaces={["Team", "common"]}>
<I18nProvider namespaces={["team", "common"]}>
<Suspense fallback={<TeamSearch.Loading />}>
<TeamSearch />
</Suspense>


+ 3
- 0
src/app/api/customer/index.ts Bestand weergeven

@@ -11,6 +11,8 @@ export interface Customer {
address: string | null;
district: string | null;
customerType: CustomerType;

contacts: Contact[];
}

export interface SaveCustomerResponse {
@@ -40,6 +42,7 @@ export interface Subsidiary {
district: string | null;
email: string | null;
subsidiaryType: SubsidiaryType;
subsidiaryContacts: Contact[];
}

export interface SubsidiaryTable {


+ 10
- 6
src/app/api/group/actions.ts Bestand weergeven

@@ -36,16 +36,20 @@ export const fetchAuth = cache(async (target: string, id?: number ) => {
});
export const saveGroup = async (data: CreateGroupInputs) => {
return serverFetchJson(`${BASE_API_URL}/group/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
const newGroup = serverFetchJson(`${BASE_API_URL}/group/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
revalidateTag("group")
return newGroup
};
export const deleteGroup = async (id: number) => {
return serverFetchWithNoContent(`${BASE_API_URL}/group/${id}`, {
const newGroup = serverFetchWithNoContent(`${BASE_API_URL}/group/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
revalidateTag("group")
return newGroup
};

+ 6
- 1
src/app/api/projects/actions.ts Bestand weergeven

@@ -20,6 +20,8 @@ export interface CreateProjectInputs {
projectLeadId: number;
projectActualStart: string;
projectActualEnd: string;
projectStatus: string;
isClpProject: boolean;

// Project info
serviceTypeId: number;
@@ -28,11 +30,14 @@ export interface CreateProjectInputs {
locationId: number;
buildingTypeIds: number[];
workNatureIds: number[];
taskTemplateId?: number | "All";

// Client details
clientId: Customer["id"];
clientContactId: number;
clientContactId?: number;
clientSubsidiaryId?: number;
subsidiaryContactId: number;
isSubsidiaryContact?: boolean;

// Allocation
totalManhour: number;


+ 1
- 1
src/app/api/projects/index.ts Bestand weergeven

@@ -12,6 +12,7 @@ export interface ProjectResult {
category: string;
team: string;
client: string;
status: string;
}

export interface ProjectCategory {
@@ -67,7 +68,6 @@ export interface AssignedProject extends ProjectWithTasks {
hoursSpent: number;
hoursSpentOther: number;
hoursAllocated: number;
hoursAllocatedOther: number;
}

export const preloadProjects = () => {


+ 14
- 1
src/app/api/reports/actions.ts Bestand weergeven

@@ -1,7 +1,7 @@
"use server";

import { serverFetchBlob, serverFetchJson } from "@/app/utils/fetchUtil";
import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest } from ".";
import { MonthlyWorkHoursReportRequest, ProjectCashFlowReportRequest,LateStartReportRequest } from ".";
import { BASE_API_URL } from "@/config/api";

export interface FileResponse {
@@ -32,5 +32,18 @@ export const fetchMonthlyWorkHoursReport = async (data: MonthlyWorkHoursReportRe
},
);

return reportBlob
};

export const fetchLateStartReport = async (data: LateStartReportRequest) => {
const reportBlob = await serverFetchBlob<FileResponse>(
`${BASE_API_URL}/reports/downloadLateStartReport`,
{
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
},
);

return reportBlob
};

+ 11
- 0
src/app/api/reports/index.ts Bestand weergeven

@@ -24,3 +24,14 @@ export interface MonthlyWorkHoursReportRequest {
yearMonth: string;
}

export interface LateStartReportFilter {
remainedDays: number;
overdueDays: number;
team: string[];
}

export interface LateStartReportRequest {
team: string[];
client: string[];
date: any;
}

+ 19
- 6
src/app/api/skill/actions.ts Bestand weergeven

@@ -1,8 +1,9 @@

"use server"

import { serverFetchJson } from "@/app/utils/fetchUtil";
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { cache } from "react";

export interface CreateSkillInputs {
@@ -29,9 +30,21 @@ export const fetchSkillCombo = cache(async () => {

export const saveSkill = async (data: CreateSkillInputs) => {
return serverFetchJson(`${BASE_API_URL}/skill/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
const newSkill = serverFetchJson(`${BASE_API_URL}/skill/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
revalidateTag("skill")
return newSkill
};

export const deleteSkill = async (id: number) => {
const newSkill = await serverFetchWithNoContent(`${BASE_API_URL}/skill/delete/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
revalidateTag("skill");
return newSkill
};

+ 4
- 3
src/app/api/skill/index.ts Bestand weergeven

@@ -4,11 +4,12 @@ import { cache } from "react";
import "server-only";

export interface SkillResult {
action: any;
action: unknown;
id: number;
name: string;
description: string;
code: string;
delete: unknown
}

export const preloadSkill = () => {
@@ -17,12 +18,12 @@ export interface SkillResult {

export const fetchSkill = cache(async () => {
return serverFetchJson<SkillResult[]>(`${BASE_API_URL}/skill`, {
next: { tags: ["sill"] },
next: { tags: ["skill"] },
});
});

export const fetchSkillDetail = cache(async (id: number) => {
return serverFetchJson<SkillResult[]>(`${BASE_API_URL}/skill/${id}`, {
next: { tags: ["sill"] },
next: { tags: ["skill"] },
});
});

+ 23
- 10
src/app/api/staff/actions.ts Bestand weergeven

@@ -1,9 +1,10 @@
"use server";
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { ServerFetchError, serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api";
import { StaffResult, data } from ".";
import { cache } from "react";
import { Team, staff } from "../team/actions";
import { revalidateTag } from "next/cache";
export interface CreateCustomInputs {
// Project details
projectCode: string;
@@ -41,17 +42,27 @@ export interface CreateStaffInputs {
name: string;
// team: Team[];
}
// export interface Staff4TransferList {
// records: records[];
// }

export const saveStaff = async (data: CreateStaffInputs) => {
return serverFetchJson(`${BASE_API_URL}/staffs/save`, {
// try {
const newStaffList = await serverFetchJson(`${BASE_API_URL}/staffs/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
console.log(newStaffList)
revalidateTag("staffs");
return newStaffList
// } catch (e: any) {
// console.log(e.response)
// throw new ServerFetchError(
// "Something went wrong fetching data in serverssssss.",
// e.response,
// );
// }
};

export const testing = async (data: CreateStaffInputs) => {
return serverFetchJson(`${BASE_API_URL}/staffs/testing`, {
@@ -62,11 +73,13 @@ export const testing = async (data: CreateStaffInputs) => {
};
export const deleteStaff = async (id: number) => {
return serverFetchWithNoContent(`${BASE_API_URL}/staffs/delete/${id}`, {
method: "DELETE",
// body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
const newStaffList = await serverFetchWithNoContent(`${BASE_API_URL}/staffs/delete/${id}`, {
method: "DELETE",
// body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
revalidateTag("staffs");
return newStaffList
};



+ 1
- 0
src/app/api/staff/index.ts Bestand weergeven

@@ -38,6 +38,7 @@ export interface StaffResult {
data: data;
teamId: number;
staffName: string;
userId: number;
}
export interface searchInput {
staffId: string;


+ 6
- 2
src/app/api/subsidiary/index.ts Bestand weergeven

@@ -10,7 +10,9 @@ export interface Customer {
brNo: string | null;
address: string | null;
district: string | null;
customerType: CustomerType
customerType: CustomerType;
contacts: Contact[];
}

export interface CustomerTable {
@@ -40,7 +42,9 @@ export interface Subsidiary {
brNo: string | null;
address: string | null;
district: string | null;
subsidiaryType: SubsidiaryType
subsidiaryType: SubsidiaryType;

contacts: Contact[];
}

export interface SaveSubsidiaryResponse {


+ 11
- 6
src/app/api/team/actions.ts Bestand weergeven

@@ -3,6 +3,7 @@ import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil
import { BASE_API_URL } from "@/config/api";
import { cache } from "react";
import { TeamResult } from ".";
import { revalidateTag } from "next/cache";


export interface CreateTeamInputs {
@@ -45,17 +46,21 @@ export const fetchTeamCombo = cache(async () => {
});

export const saveTeam = async (data: CreateTeamInputs) => {
return serverFetchJson(`${BASE_API_URL}/team/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
const newTeam = serverFetchJson(`${BASE_API_URL}/team/save`, {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
revalidateTag("team")
return newTeam
};

export const deleteTeam = async (id: number) => {
return serverFetchWithNoContent(`${BASE_API_URL}/team/delete/${id}`, {
const newTeam = serverFetchWithNoContent(`${BASE_API_URL}/team/delete/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
revalidateTag("team")
return newTeam
};

+ 13
- 9
src/app/api/user/actions.ts Bestand weergeven

@@ -26,18 +26,22 @@ export const fetchUserDetails = cache(async (id: number) => {
});

export const editUser = async (id: number, data: UserInputs) => {
return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
method: "PUT",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
method: "PUT",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
revalidateTag("user")
return newUser
};

export const deleteUser = async (id: number) => {
return serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
const newUser = serverFetchWithNoContent(`${BASE_API_URL}/user/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
});
revalidateTag("user")
return newUser
};

export const changePassword = async (data: any) => {


+ 8
- 3
src/app/utils/fetchUtil.ts Bestand weergeven

@@ -38,18 +38,23 @@ type FetchParams = Parameters<typeof fetch>;

export async function serverFetchJson<T>(...args: FetchParams) {
const response = await serverFetch(...args);

if (response.ok) {
return response.json() as T;
} else {
const errorText = await response.text()
switch (response.status) {
case 401:
signOutUser();
case 422:
throw new ServerFetchError(
JSON.parse(errorText).error,
response
);
default:
console.error(await response.text());
console.error(errorText);
throw new ServerFetchError(
"Something went wrong fetching data in server.",
response,
response
);
}
}


+ 65
- 24
src/app/utils/formatUtil.ts Bestand weergeven

@@ -21,41 +21,52 @@ export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD";

export const OUTPUT_TIME_FORMAT = "HH:mm:ss";

export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FORMAT) => {
return dayjs(date).format(format)
}
export const convertDateToString = (
date: Date,
format: string = OUTPUT_DATE_FORMAT,
) => {
return dayjs(date).format(format);
};

export const convertDateArrayToString = (dateArray: number[], format: string = OUTPUT_DATE_FORMAT, needTime: boolean = false) => {
export const convertDateArrayToString = (
dateArray: number[],
format: string = OUTPUT_DATE_FORMAT,
needTime: boolean = false,
) => {
if (dateArray.length === 6) {
if (!needTime) {
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`
return dayjs(dateString).format(format)
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`;
return dayjs(dateString).format(format);
}
}
if (dateArray.length === 3) {
if (!needTime) {
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`
return dayjs(dateString).format(format)
const dateString = `${dateArray[0]}-${dateArray[1]}-${dateArray[2]}`;
return dayjs(dateString).format(format);
}
}
}
};

export const convertTimeArrayToString = (timeArray: number[], format: string = OUTPUT_TIME_FORMAT, needTime: boolean = false) => {
let timeString = '';
export const convertTimeArrayToString = (
timeArray: number[],
format: string = OUTPUT_TIME_FORMAT,
needTime: boolean = false,
) => {
let timeString = "";

if (timeArray !== null && timeArray !== undefined) {
const hour = timeArray[0] || 0;
const minute = timeArray[1] || 0;
timeString = dayjs()
.set('hour', hour)
.set('minute', minute)
.set('second', 0)
.format('HH:mm:ss');
const hour = timeArray[0] || 0;
const minute = timeArray[1] || 0;
timeString = dayjs()
.set("hour", hour)
.set("minute", minute)
.set("second", 0)
.format("HH:mm:ss");
}
return timeString
}
return timeString;
};

const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", {
weekday: "short",
@@ -81,6 +92,36 @@ export const shortDateFormatter = (locale?: string) => {
}
};

const clockFormatOptions: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "long",
day: "numeric",
weekday: "long",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: true,
};

const clockTimeFormatter_en = new Intl.DateTimeFormat(
"en-HK",
clockFormatOptions,
);
const clockTimeformatter_zh = new Intl.DateTimeFormat(
"zh-HK",
clockFormatOptions,
);

export const clockTimeFormatter = (locale?: string) => {
switch (locale) {
case "zh":
return clockTimeformatter_zh;
case "en":
default:
return clockTimeFormatter_en;
}
};

export function convertLocaleStringToNumber(numberString: string): number {
const numberWithoutCommas = numberString.replace(/,/g, "");
return parseFloat(numberWithoutCommas);
@@ -91,6 +132,6 @@ export function timestampToDateString(timestamp: string): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
console.log(`${year}-${month}-${day}`)
console.log(`${year}-${month}-${day}`);
return `${year}-${month}-${day}`;
}
}

+ 14
- 3
src/components/AppBar/Profile.tsx Bestand weergeven

@@ -10,7 +10,7 @@ import Divider from "@mui/material/Divider";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import { signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import { usePathname, useRouter, useSearchParams } from "next/navigation";

type Props = Pick<AppBarProps, "avatarImageSrc" | "profileName">;

@@ -26,8 +26,17 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => {
setProfileMenuAnchorEl(undefined);
};

const { t } = useTranslation("login");
const { t, i18n: { language } } = useTranslation("login");
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams()

const onLangClick = React.useCallback((lang: string) => {
const params = new URLSearchParams(searchParams.toString())
params.set("lang", lang)
router.replace(`${pathname}?${params.toString()}`);
window.location.reload();
}, [router, pathname, searchParams]);

return (
<>
@@ -54,7 +63,9 @@ const Profile: React.FC<Props> = ({ avatarImageSrc, profileName }) => {
{profileName}
</Typography>
<Divider />
<MenuItem onClick={() => {router.replace("/settings/changepassword")}}>{t("Change Password")}</MenuItem>
<MenuItem onClick={() => { router.replace("/settings/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>
</Menu>
</>


+ 36
- 35
src/components/Breadcrumb/Breadcrumb.tsx Bestand weergeven

@@ -7,7 +7,7 @@ import MUILink from "@mui/material/Link";
import { usePathname } from "next/navigation";
import { useTranslation } from "react-i18next";
import Clock from "./Clock";
import { Grid } from "@mui/material";
import { Box, Grid } from "@mui/material";
import { I18nProvider } from "@/i18n";

const pathToLabelMap: { [path: string]: string } = {
@@ -46,42 +46,43 @@ const Breadcrumb = () => {

// const { t } = useTranslation("customer");


return (
<Grid container>
<Grid item xs={6}>
<Breadcrumbs>
{segments.map((segment, index) => {
const href = segments.slice(0, index + 1).join("/");
const label = pathToLabelMap[href] || segment;
<Box
display="flex"
flexDirection={{ xs: "column-reverse", sm: "row"}}
justifyContent={{ sm: "space-between" }}
>
<Breadcrumbs>
{segments.map((segment, index) => {
const href = segments.slice(0, index + 1).join("/");
const label = pathToLabelMap[href] || segment;

if (index === segments.length - 1) {
return (
<Typography key={index} color="text.primary">
{label}
{/* {t(label)} */}
</Typography>
);
} else {
return (
<MUILink
underline="hover"
color="inherit"
key={index}
component={Link}
href={href || "/"}
>
{label}
</MUILink>
);
}
})}
</Breadcrumbs>
</Grid>
<Grid item xs={6} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Clock />
</Grid>
</Grid>
if (index === segments.length - 1) {
return (
<Typography key={index} color="text.primary">
{label}
{/* {t(label)} */}
</Typography>
);
} else {
return (
<MUILink
underline="hover"
color="inherit"
key={index}
component={Link}
href={href || "/"}
>
{label}
</MUILink>
);
}
})}
</Breadcrumbs>
<Box width={{ xs: "100%", sm: "auto" }} marginBlockEnd={{ xs: 1, sm: 0 }}>
<Clock variant="body2" />
</Box>
</Box>
);
};



+ 26
- 25
src/components/Breadcrumb/Clock.tsx Bestand weergeven

@@ -1,32 +1,33 @@
"use client"
import { useState, useEffect, useLayoutEffect } from 'react';
import Typography from "@mui/material/Typography";
import { useTranslation } from 'react-i18next';
"use client";
import React, { useState, useLayoutEffect } from "react";
import Typography, { TypographyProps } from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import { clockTimeFormatter } from "@/app/utils/formatUtil";
import { NoSsr } from "@mui/material";

const Clock = () => {
const {
i18n: { language },
} = useTranslation();
const [currentDateTime, setCurrentDateTime] = useState(new Date());
const Clock: React.FC<TypographyProps> = (props) => {
const {
i18n: { language },
} = useTranslation();
const [currentDateTime, setCurrentDateTime] = useState(new Date());

useLayoutEffect(() => {
const timer = setInterval(() => {
setCurrentDateTime(new Date());
}, 1000);
useLayoutEffect(() => {
const timer = setInterval(() => {
setCurrentDateTime(new Date());
}, 1000);

return () => {
clearInterval(timer);
};
}, []);
return () => {
clearInterval(timer);
};
}, []);

const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true };
const formattedDateTime = new Intl.DateTimeFormat(language, options).format(currentDateTime)

return (
<Typography color="text.primary" suppressHydrationWarning>
{formattedDateTime}
</Typography>
);
return (
<NoSsr>
<Typography {...props}>
{clockTimeFormatter(language).format(currentDateTime)}
</Typography>
</NoSsr>
);
};

export default Clock;

+ 0
- 1
src/components/ChangePassword/ChangePassword.tsx Bestand weergeven

@@ -8,7 +8,6 @@ import { useTranslation } from "react-i18next";
import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material";
import { Check, Close, Error } from "@mui/icons-material";
import ChagnePasswordForm from "./ChangePasswordForm";
import { ServerFetchError } from "@/app/utils/fetchUtil";

// interface Props {
// // auth?: auth[]


+ 15
- 8
src/components/CreateProject/CreateProject.tsx Bestand weergeven

@@ -76,7 +76,7 @@ const hasErrorsInTab = (
switch (tabIndex) {
case 0:
return (
errors.projectName || errors.projectCode || errors.projectDescription
errors.projectName || errors.projectDescription || errors.clientId
);
case 2:
return (
@@ -219,6 +219,7 @@ const CreateProject: React.FC<Props> = ({
data.projectActualEnd = dayjs().format("YYYY-MM-DD");
}

data.taskTemplateId = data.taskTemplateId === "All" ? undefined : data.taskTemplateId;
const response = await saveProject(data);

if (response.id > 0) {
@@ -248,7 +249,8 @@ const CreateProject: React.FC<Props> = ({
if (
errors.projectName ||
errors.projectDescription ||
errors.projectCode
// errors.projectCode ||
errors.clientId
) {
setTabIndex(0);
} else if (errors.totalManhour || errors.manhourPercentageByGrade || errors.taskGroups) {
@@ -266,6 +268,7 @@ const CreateProject: React.FC<Props> = ({
allocatedStaffIds: [],
milestones: {},
totalManhour: 0,
taskTemplateId: "All",
...defaultInputs,

// manhourPercentageByGrade should have a sensible default
@@ -289,7 +292,8 @@ const CreateProject: React.FC<Props> = ({
>
{isEditMode && !(formProps.getValues("projectDeleted") === true) && (
<Stack direction="row" gap={1}>
{!formProps.getValues("projectActualStart") && (
{/* {!formProps.getValues("projectActualStart") && ( */}
{formProps.getValues("projectStatus") === "Pending to Start" && (
<Button
name="start"
type="submit"
@@ -300,8 +304,9 @@ const CreateProject: React.FC<Props> = ({
{t("Start Project")}
</Button>
)}
{formProps.getValues("projectActualStart") &&
!formProps.getValues("projectActualEnd") && (
{/* {formProps.getValues("projectActualStart") &&
!formProps.getValues("projectActualEnd") && ( */}
{formProps.getValues("projectStatus") === "On-going" && (
<Button
name="complete"
type="submit"
@@ -313,8 +318,10 @@ const CreateProject: React.FC<Props> = ({
</Button>
)}
{!(
formProps.getValues("projectActualStart") &&
formProps.getValues("projectActualEnd")
// formProps.getValues("projectActualStart") &&
// formProps.getValues("projectActualEnd")
formProps.getValues("projectStatus") === "Completed" ||
formProps.getValues("projectStatus") === "Deleted"
) && (
<Button
variant="outlined"
@@ -412,7 +419,7 @@ const CreateProject: React.FC<Props> = ({
startIcon={<Check />}
type="submit"
disabled={
formProps.getValues("projectDeleted") === true ||
formProps.getValues("projectDeleted") === true || formProps.getValues("projectStatus") === "Deleted" ||
(!!formProps.getValues("projectActualStart") &&
!!formProps.getValues("projectActualEnd"))
}


+ 31
- 23
src/components/CreateProject/MilestoneSection.tsx Bestand weergeven

@@ -58,8 +58,8 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
const apiRef = useGridApiRef();
const addRow = useCallback(() => {
// const id = Date.now();
const minId = Math.min(...payments.map((payment) => payment.id!!));
const id = minId >= 0 ? -1 : minId - 1
const minId = Math.min(...payments.map((payment) => payment.id!));
const id = minId >= 0 ? -1 : minId - 1;
setPayments((p) => [...p, { id, _isNew: true }]);
setRowModesModel((model) => ({
...model,
@@ -241,26 +241,30 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs>
<FormControl fullWidth>
<DatePicker
label={t("Stage Start Date")}
value={startDate ? dayjs(startDate) : null}
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");
setValue("milestones", {
...milestones,
[taskGroupId]: {
...milestones[taskGroupId],
startDate: date.format(INPUT_DATE_FORMAT),
},
});
}}
slotProps={{
textField: {
error: startDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(startDate)),
},
}}
/>
<DatePicker
label={t("Stage Start Date")}
value={startDate ? dayjs(startDate) : null}
onChange={(date) => {
if (!date) return;
const milestones = getValues("milestones");
setValue("milestones", {
...milestones,
[taskGroupId]: {
...milestones[taskGroupId],
startDate: date.format(INPUT_DATE_FORMAT),
},
});
}}
slotProps={{
textField: {
error:
startDate === "Invalid Date" ||
new Date(startDate) > new Date(endDate) ||
(Boolean(formState.errors.milestones) &&
!Boolean(startDate)),
},
}}
/>
</FormControl>
</Grid>
<Grid item xs>
@@ -281,7 +285,11 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => {
}}
slotProps={{
textField: {
error: endDate === "Invalid Date" || new Date(startDate) > new Date(endDate) || (Boolean(formState.errors.milestones) && !Boolean(endDate)),
error:
endDate === "Invalid Date" ||
new Date(startDate) > new Date(endDate) ||
(Boolean(formState.errors.milestones) &&
!Boolean(endDate)),
},
}}
/>


+ 117
- 66
src/components/CreateProject/ProjectClientDetails.tsx Bestand weergeven

@@ -85,6 +85,7 @@ const ProjectClientDetails: React.FC<Props> = ({
);

const [customerContacts, setCustomerContacts] = useState<Contact[]>([]);
const [subsidiaryContacts, setSubsidiaryContacts] = useState<Contact[]>([]);
const [customerSubsidiaryIds, setCustomerSubsidiaryIds] = useState<number[]>(
[],
);
@@ -92,21 +93,44 @@ const ProjectClientDetails: React.FC<Props> = ({
const selectedCustomerContactId = watch("clientContactId");
const selectedCustomerContact = useMemo(
() =>
customerContacts.find(
(contact) => contact.id === selectedCustomerContactId,
),
[customerContacts, selectedCustomerContactId],
subsidiaryContacts.length > 0 ?
subsidiaryContacts.find((contact) => contact.id === selectedCustomerContactId)
: customerContacts.find(
(contact) => contact.id === selectedCustomerContactId,
),
[subsidiaryContacts, customerContacts, selectedCustomerContactId],
);

// get customer (client) contact combo
useEffect(() => {
if (selectedCustomerId !== undefined) {
fetchCustomer(selectedCustomerId).then(({ contacts, subsidiaryIds }) => {
setCustomerContacts(contacts);
setCustomerSubsidiaryIds(subsidiaryIds);

if (subsidiaryIds.length > 0) setValue("clientSubsidiaryId", subsidiaryIds[0])
else setValue("clientSubsidiaryId", undefined)
// if (contacts.length > 0) setValue("clientContactId", contacts[0].id)
// else setValue("clientContactId", undefined)
});
}
}, [selectedCustomerId]);

const clientSubsidiaryId = watch("clientSubsidiaryId")
useEffect(() => {
if (Boolean(clientSubsidiaryId)) {
// get subsidiary contact combo
const contacts = allSubsidiaries.find(subsidiary => subsidiary.id === clientSubsidiaryId)?.subsidiaryContacts!!
setSubsidiaryContacts(contacts)
setValue("clientContactId", contacts[0].id)
setValue("isSubsidiaryContact", true)
} else if (customerContacts?.length > 0) {
setSubsidiaryContacts([])
setValue("clientContactId", customerContacts[0].id)
setValue("isSubsidiaryContact", false)
}
}, [customerContacts, clientSubsidiaryId]);

// Automatically add the team lead to the allocated staff list
const selectedTeamLeadId = watch("projectLeadId");
useEffect(() => {
@@ -139,10 +163,13 @@ const ProjectClientDetails: React.FC<Props> = ({
<TextField
label={t("Project Code")}
fullWidth
{...register("projectCode", {
required: "Project code required!",
})}
error={Boolean(errors.projectCode)}
disabled
{...register("projectCode",
// {
// required: "Project code required!",
// }
)}
// error={Boolean(errors.projectCode)}
/>
</Grid>
<Grid item xs={6}>
@@ -354,6 +381,16 @@ const ProjectClientDetails: React.FC<Props> = ({
{...register("expectedProjectFee", { valueAsNumber: true })}
/>
</Grid>

<Grid item xs={6}>
<Checkbox
{...register("isClpProject")}
defaultChecked={watch("isClpProject")}
/>
<Typography variant="overline" display="inline">
{t("CLP Project")}
</Typography>
</Grid>
</Grid>
</Box>

@@ -373,12 +410,15 @@ const ProjectClientDetails: React.FC<Props> = ({
</Stack>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<FormControl fullWidth>
<FormControl fullWidth error={Boolean(errors.clientId)}>
<InputLabel>{t("Client")}</InputLabel>
<Controller
defaultValue={allCustomers[0].id}
defaultValue={allCustomers[0]?.id}
control={control}
name="clientId"
rules={{
required: "Please select a client",
}}
render={({ field }) => (
<Select label={t("Client")} {...field}>
{allCustomers.map((customer, index) => (
@@ -408,6 +448,50 @@ const ProjectClientDetails: React.FC<Props> = ({
<Grid item sx={{ display: { xs: "none", sm: "block" } }} />
{customerContacts.length > 0 && (
<>
{customerSubsidiaryIds.length > 0 && (
<Grid item xs={6}>
<FormControl
fullWidth
error={Boolean(errors.clientSubsidiaryId)}
>
<InputLabel>{t("Client Subsidiary")}</InputLabel>
<Controller
// rules={{
// validate: (value) => {
// if (
// !customerSubsidiaryIds.find(
// (subsidiaryId) => subsidiaryId === value,
// )
// ) {
// return t("Please choose a valid subsidiary");
// } else return true;
// },
// }}
defaultValue={customerSubsidiaryIds[0]}
control={control}
name="clientSubsidiaryId"
render={({ field }) => (
<Select label={t("Client Subsidiary")} {...field}>
{customerSubsidiaryIds
.filter((subId) => subsidiaryMap[subId])
.map((subsidiaryId, index) => {
const subsidiary = subsidiaryMap[subsidiaryId];

return (
<MenuItem
key={`${subsidiaryId}-${index}`}
value={subsidiaryId}
>
{`${subsidiary.code} - ${subsidiary.name}`}
</MenuItem>
);
})}
</Select>
)}
/>
</FormControl>
</Grid>
)}
<Grid item xs={6}>
<FormControl
fullWidth
@@ -418,33 +502,44 @@ const ProjectClientDetails: React.FC<Props> = ({
rules={{
validate: (value) => {
if (
!customerContacts.find(
(customerContacts.length > 0 && !customerContacts.find(
(contact) => contact.id === value,
)) && (subsidiaryContacts?.length > 0 && !subsidiaryContacts.find(
(contact) => contact.id === value,
)
))
) {
return t("Please provide a valid contact");
} else return true;
},
}}
defaultValue={customerContacts[0].id}
defaultValue={subsidiaryContacts?.length > 0 ? subsidiaryContacts[0].id : customerContacts[0].id}
control={control}
name="clientContactId"
render={({ field }) => (
<Select label={t("Client Lead")} {...field}>
{customerContacts.map((contact, index) => (
<MenuItem
key={`${contact.id}-${index}`}
value={contact.id}
>
{contact.name}
</MenuItem>
))}
{subsidiaryContacts?.length > 0 ?
subsidiaryContacts.map((contact, index) => (
<MenuItem
key={`${contact.id}-${index}`}
value={contact.id}
>
{contact.name}
</MenuItem>
))
: customerContacts.map((contact, index) => (
<MenuItem
key={`${contact.id}-${index}`}
value={contact.id}
>
{contact.name}
</MenuItem>
))}
</Select>
)}
/>
</FormControl>
</Grid>
<Grid item sx={{ display: { xs: "none", sm: "block" } }} />
<Grid container sx={{ display: { xs: "none", sm: "block" } }} />
<Grid item xs={6}>
<TextField
label={t("Client Lead Phone Number")}
@@ -467,50 +562,6 @@ const ProjectClientDetails: React.FC<Props> = ({
</Grid>
</>
)}
{customerSubsidiaryIds.length > 0 && (
<Grid item xs={6}>
<FormControl
fullWidth
error={Boolean(errors.clientSubsidiaryId)}
>
<InputLabel>{t("Client Subsidiary")}</InputLabel>
<Controller
// rules={{
// validate: (value) => {
// if (
// !customerSubsidiaryIds.find(
// (subsidiaryId) => subsidiaryId === value,
// )
// ) {
// return t("Please choose a valid subsidiary");
// } else return true;
// },
// }}
defaultValue={customerSubsidiaryIds[0]}
control={control}
name="clientSubsidiaryId"
render={({ field }) => (
<Select label={t("Client Lead")} {...field}>
{customerSubsidiaryIds
.filter((subId) => subsidiaryMap[subId])
.map((subsidiaryId, index) => {
const subsidiary = subsidiaryMap[subsidiaryId];

return (
<MenuItem
key={`${subsidiaryId}-${index}`}
value={subsidiaryId}
>
{`${subsidiary.code} - ${subsidiary.name}`}
</MenuItem>
);
})}
</Select>
)}
/>
</FormControl>
</Grid>
)}
</Grid>
</Box>
<CardActions sx={{ justifyContent: "flex-end" }}>


+ 32
- 10
src/components/CreateProject/TaskSetup.tsx Bestand weergeven

@@ -33,7 +33,7 @@ const TaskSetup: React.FC<Props> = ({
isActive,
}) => {
const { t } = useTranslation();
const { setValue, watch, clearErrors, setError } = useFormContext<CreateProjectInputs>();
const { setValue, watch, clearErrors, setError, formState: { defaultValues } } = useFormContext<CreateProjectInputs>();
const currentTaskGroups = watch("taskGroups");
const currentTaskIds = Object.values(currentTaskGroups).reduce<Task["id"][]>(
(acc, group) => {
@@ -48,7 +48,7 @@ const TaskSetup: React.FC<Props> = ({

const [selectedTaskTemplateId, setSelectedTaskTemplateId] = useState<
"All" | number
>("All");
>(watch("taskTemplateId") ?? "All");
const onSelectTaskTemplate = useCallback(
(e: SelectChangeEvent<number | "All">) => {
if (e.target.value === "All" || isNumber(e.target.value)) {
@@ -64,7 +64,8 @@ const TaskSetup: React.FC<Props> = ({
(template) => template.id === selectedTaskTemplateId,
)

if (selectedTaskTemplateId !== "All") {
if (selectedTaskTemplateId !== "All" && selectedTaskTemplateId !== watch("taskTemplateId")) {

// update the "manhour allocation by grade" by task template
const updatedManhourPercentageByGrade: ManhourAllocation = watch("manhourPercentageByGrade")
selectedTaskTemplate?.gradeAllocations.forEach((gradeAllocation) => {
@@ -73,28 +74,30 @@ const TaskSetup: React.FC<Props> = ({

setValue("manhourPercentageByGrade", updatedManhourPercentageByGrade)
if (Object.values(updatedManhourPercentageByGrade).reduce((acc, value) => acc + value, 0) === 100) clearErrors("manhourPercentageByGrade")
else setError("manhourPercentageByGrade", {message: "manhourPercentageByGrade value is not valid", type: "invalid"})
else setError("manhourPercentageByGrade", { message: "manhourPercentageByGrade value is not valid", type: "invalid" })

// update the "manhour allocation by grade by stage" by task template
const updatedTaskGroups = watch("taskGroups")
const taskGroupsKeys = Object.keys(updatedTaskGroups)
selectedTaskTemplate?.groupAllocations.forEach((groupAllocation) => {
const taskGroupId = groupAllocation.taskGroup.id
if(taskGroupsKeys.includes(taskGroupId.toString())) {
updatedTaskGroups[taskGroupId] = {...updatedTaskGroups[taskGroupId], percentAllocation: groupAllocation?.percentage}
if (taskGroupsKeys.includes(taskGroupId.toString())) {
updatedTaskGroups[taskGroupId] = { ...updatedTaskGroups[taskGroupId], percentAllocation: groupAllocation?.percentage }
}
})

const percentageToZeroGroupIds = difference(taskGroupsKeys.map(key => parseFloat(key)), selectedTaskTemplate?.groupAllocations.map(groupAllocation => groupAllocation.taskGroup.id)!!)
percentageToZeroGroupIds.forEach((percentageToZeroGroupId) => {
updatedTaskGroups[percentageToZeroGroupId] = {...updatedTaskGroups[percentageToZeroGroupId], percentAllocation: 0}
updatedTaskGroups[percentageToZeroGroupId] = { ...updatedTaskGroups[percentageToZeroGroupId], percentAllocation: 0 }
})
setValue("taskGroups", updatedTaskGroups)
if (Object.values(updatedTaskGroups).reduce((acc, value) => acc + value.percentAllocation, 0) === 100) clearErrors("taskGroups")
else setError("taskGroups", {message: "Task Groups value is not invalid", type: "invalid"})
else setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" })
}

setValue("taskTemplateId", selectedTaskTemplateId)

const taskList =
selectedTaskTemplateId === "All"
? tasks
@@ -176,7 +179,26 @@ const TaskSetup: React.FC<Props> = ({
};
}, {});

setValue("taskGroups", newTaskGroups);
// update the "manhour allocation by grade by stage" by task template
const taskGroupsKeys = Object.keys(newTaskGroups)
const selectedTaskTemplate = taskTemplates.find(
(template) => template.id === selectedTaskTemplateId,
)
selectedTaskTemplate?.groupAllocations.forEach((groupAllocation) => {
const taskGroupId = groupAllocation.taskGroup.id
if (taskGroupsKeys.includes(taskGroupId.toString())) {
newTaskGroups[taskGroupId] = { ...newTaskGroups[taskGroupId], percentAllocation: groupAllocation?.percentage }
}
})

const percentageToZeroGroupIds = difference(taskGroupsKeys.map(key => parseFloat(key)), selectedTaskTemplate?.groupAllocations.map(groupAllocation => groupAllocation.taskGroup.id)!!)
percentageToZeroGroupIds.forEach((percentageToZeroGroupId) => {
newTaskGroups[percentageToZeroGroupId] = { ...newTaskGroups[percentageToZeroGroupId], percentAllocation: 0 }
})

setValue("taskGroups", newTaskGroups)
if (Object.values(newTaskGroups).reduce((acc, value) => acc + value.percentAllocation, 0) === 100) clearErrors("taskGroups")
else setError("taskGroups", { message: "Task Groups value is not invalid", type: "invalid" })
}}
allItemsLabel={t("Task Pool")}
selectedItemsLabel={t("Project Task List")}


+ 33
- 26
src/components/CreateSkill/CreateSkill.tsx Bestand weergeven

@@ -23,7 +23,7 @@ const CreateSkill: React.FC<Props> = () => {
const [serverError, setServerError] = useState("");
const router = useRouter();
const { t } = useTranslation();
const [tabIndex, setTabIndex] = useState(0);
// const [tabIndex, setTabIndex] = useState(0);
const errors = formProps.formState.errors;

const onSubmit = useCallback<SubmitHandler<CreateSkillInputs>>(
@@ -44,28 +44,28 @@ const CreateSkill: React.FC<Props> = () => {
router.back();
};

// const handleReset = useCallback(() => {
// console.log(defaultValues)
// }, [defaultValues])
const resetSkill = useCallback(() => {
formProps.reset()
}, [])

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);
// const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
// (_e, newValue) => {
// setTabIndex(newValue);
// },
// []
// );

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<CreateSkillInputs>
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};
// const hasErrorsInTab = (
// tabIndex: number,
// errors: FieldErrors<CreateSkillInputs>
// ) => {
// switch (tabIndex) {
// case 0:
// return Object.keys(errors).length > 0;
// default:
// false;
// }
// };
return (
<>
<FormProvider {...formProps}>
@@ -74,13 +74,13 @@ const CreateSkill: React.FC<Props> = () => {
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Tabs
{/* <Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
>
<Tab
label={t("Team Info")}
label={t("Skill Info")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
@@ -88,15 +88,22 @@ const CreateSkill: React.FC<Props> = () => {
}
iconPosition="end"
/>
{/* <Tab label={t("Certification")} iconPosition="end" /> */}
</Tabs>
</Tabs> */}
{serverError && (
<Typography variant="body2" color="error" alignSelf="flex-end">
{serverError}
</Typography>
)}
{tabIndex === 0 && <SkillInfo />}
{/* {tabIndex === 0 && <SkillInfo />} */}
<SkillInfo />
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"
startIcon={<RestartAlt />}
onClick={resetSkill}
>
{t("Reset")}
</Button>
<Button
variant="outlined"
startIcon={<Close />}


+ 56
- 48
src/components/CreateSkill/SkillInfo.tsx Bestand weergeven

@@ -15,8 +15,7 @@ import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import { useCallback } from "react";
import { CreateSkillInputs } from "@/app/api/skill/actions";

const SkillInfo: React.FC = (
) => {
const SkillInfo: React.FC = () => {
const { t } = useTranslation();
const {
register,
@@ -27,13 +26,6 @@ const SkillInfo: React.FC = (
setValue,
} = useFormContext<CreateSkillInputs>();

const resetSkill = useCallback(() => {
console.log(defaultValues);
if (defaultValues !== undefined) {
resetField("name");
}
}, [defaultValues]);
return (
<>
<Card sx={{ display: "block" }}>
@@ -42,45 +34,61 @@ const SkillInfo: React.FC = (
<Typography variant="overline" display="block" marginBlockEnd={1}>
{t("Skill Info")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Skill Name")}
fullWidth
rows={4}
{...register("name", {
required: true,
})}
error={Boolean(errors.name)}
helperText={Boolean(errors.name) && (errors.name?.message ? t(errors.name.message) : t("Please input correct name"))}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Skill Code")}
fullWidth
rows={4}
{...register("code", {
required: true,
})}
error={Boolean(errors.code)}
helperText={Boolean(errors.code) && (errors.code?.message ? t(errors.code.message) : t("Please input correct name"))}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Skill Description")}
fullWidth
multiline
rows={4}
{...register("description", {
required: true,
})}
error={Boolean(errors.description)}
helperText={Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) : t("Please input correct name"))}
/>
</Grid>
</Grid>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6}>
<TextField
label={t("Skill Name")}
fullWidth
rows={4}
{...register("name", {
required: true,
})}
error={Boolean(errors.name)}
helperText={
Boolean(errors.name) &&
(errors.name?.message
? t(errors.name.message)
: `${t("Please input correct ")}${t("name")}`

)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Code")}
fullWidth
rows={4}
{...register("code", {
required: true,
})}
error={Boolean(errors.code)}
helperText={
Boolean(errors.code) &&
(errors.code?.message
? t(errors.code.message)
: `${t("Please input correct ")}${t("code")}`
)}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Description")}
fullWidth
multiline
rows={4}
{...register("description", {
required: true,
})}
error={Boolean(errors.description)}
helperText={
Boolean(errors.description) &&
(errors.description?.message
? t(errors.description.message)
: `${t("Please input correct ")}${t("description")}`
)}
/>
</Grid>
</Grid>
</Box>
</CardContent>
</Card>


+ 20
- 10
src/components/CreateStaff/CreateStaff.tsx Bestand weergeven

@@ -22,6 +22,7 @@ import { fetchSkillCombo } from "@/app/api/skill/actions";
import { fetchSalaryCombo } from "@/app/api/salarys/actions";
import StaffInfo from "./StaffInfo";
import { Check, Close } from "@mui/icons-material";
import { ServerFetchError } from "@/app/utils/fetchUtil";

interface Field {
id: string;
@@ -59,6 +60,10 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => {

const errors = formProps.formState.errors;

const checkDuplicates = (str1: string, str2: string, str3: string) => {
return str1 === str2 || str1 === str3 || str2 === str3;
}

const onSubmit = useCallback<SubmitHandler<CreateStaffInputs>>(
async (data) => {
try {
@@ -85,6 +90,14 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => {
formProps.setError("phone2", { message: t("Please Enter Correct Phone No.."), type: "required" })
}
}
if (data.phone1 === data.phone2 || data.phone1 === data.emergContactPhone || data.phone2 === data.emergContactPhone) {
haveError = true
formProps.setError("phone1", { message: t("Please Enter Different Phone No.."), type: "required" })
if (data.phone2!.length > 0) {
formProps.setError("phone2", { message: t("Please Enter Different Phone No.."), type: "required" })
}
formProps.setError("emergContactPhone", { message: t("Please Enter Different Phone No.."), type: "required" })
}
if (!regex_email.test(data.email)) {
haveError = true
formProps.setError("email", { message: t("Please Enter Correct Email."), type: "required" })
@@ -93,10 +106,6 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => {
haveError = true
formProps.setError("companyId", { message: t("Please Enter Company."), type: "required" })
}
if (!data.gradeId) {
haveError = true
formProps.setError("gradeId", { message: t("Please Enter grade."), type: "required" })
}
if (!data.employType) {
haveError = true
formProps.setError("employType", { message: t("Please Enter Employ Type."), type: "required" })
@@ -117,19 +126,20 @@ const CreateStaff: React.FC<formProps> = ({ combos }) => {
haveError = true
formProps.setError("departDate", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" })
}
// if (!data.joinPositionId) {
// haveError = true
// formProps.setError("joinPositionId", { message: t("Depart Date cannot be earlier than Join Date."), type: "required" })
// }
if (haveError) {
return
}
console.log("passed")
await saveStaff(data)
router.replace("/settings/staff")
} catch (e) {
} catch (e: any) {
console.log(e);
setServerError(t("An error has occurred. Please try again later."));
formProps.setError("staffId", { message: t("Please Enter Employ Type."), type: "required" })
let msg = ""
if (e.message === "Duplicated StaffId Found") {
msg = t("Duplicated StaffId Found")
}
setServerError(`${t("An error has occurred. Please try again later.")} ${msg} `);
}
},
[router]


+ 19
- 18
src/components/CreateStaff/StaffInfo.tsx Bestand weergeven

@@ -164,7 +164,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Department")}</InputLabel>
<InputLabel required>{t("Department")}</InputLabel>
<Controller
control={control}
name="departmentId"
@@ -189,7 +189,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel required>{t("Grade")}</InputLabel>
<InputLabel>{t("Grade")}</InputLabel>
<Controller
control={control}
name="gradeId"
@@ -272,7 +272,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Salary Point")}</InputLabel>
<InputLabel required>{t("Salary Point")}</InputLabel>
<Controller
control={control}
name="salaryId"
@@ -297,7 +297,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Employ Type")}</InputLabel>
<InputLabel required>{t("Employ Type")}</InputLabel>
<Controller
control={control}
name="employType"
@@ -408,28 +408,29 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
>
<DatePicker
sx={{ width: "100%" }}
label={t("Join Date")}
value={joinDate ? dayjs(joinDate) : null}
onChange={(date) => {
if (!date) return;
setValue("joinDate", date.format(INPUT_DATE_FORMAT));
}}
slotProps={{
textField: {
error:
joinDate === "Invalid Date" || Boolean(errors.joinDate),
// value: errors.joinDate?.message,
},
}}
/>
if (!date) return;
setValue("joinDate", date.format(INPUT_DATE_FORMAT));
}}
slotProps={{
textField: {
required: true,
error:
joinDate === "Invalid Date" || Boolean(errors.joinDate),
// value: errors.joinDate?.message,
},
}}
/>
</LocalizationProvider>
</Grid>
<Grid item xs={6}>
<FormControl fullWidth>
<InputLabel>{t("Join Position")}</InputLabel>
<InputLabel required>{t("Join Position")}</InputLabel>
<Controller
control={control}
name="joinPositionId"
@@ -442,7 +443,7 @@ const StaffInfo: React.FC<Props> = ({ combos }) => {
{combos.position.map((position, index) => (
<MenuItem
key={`${position.id}-${index}`}
value={position.label}
value={position.id}
>
{t(position.label)}
</MenuItem>


+ 3
- 6
src/components/CreateTeam/CreateTeam.tsx Bestand weergeven

@@ -1,5 +1,4 @@
"use client";

import {
FieldErrors,
FormProvider,
@@ -13,7 +12,7 @@ import { CreateTeamInputs, saveTeam } from "@/app/api/team/actions";
import { Button, Stack, Tab, Tabs, TabsProps, Typography } from "@mui/material";
import { Check, Close } from "@mui/icons-material";
import { useCallback, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
import { Error } from "@mui/icons-material";
import TeamInfo from "./TeamInfo";
@@ -28,7 +27,7 @@ const CreateTeam: React.FC<Props> = ({ allstaff }) => {
const router = useRouter();
const [tabIndex, setTabIndex] = useState(0);
const { t } = useTranslation();
const searchParams = useSearchParams()
// const searchParams = useSearchParams()

const errors = formProps.formState.errors;

@@ -81,7 +80,7 @@ const hasErrorsInTab = (
variant="scrollable"
>
<Tab
label={t("Team Info")}
label={t("Team Info")}
icon={
hasErrorsInTab(0, errors) ? (
<Error sx={{ marginInlineEnd: 1 }} color="error" />
@@ -98,8 +97,6 @@ const hasErrorsInTab = (
)}
{tabIndex === 0 && <TeamInfo/>}
{tabIndex === 1 && <StaffAllocation allStaffs={allstaff} />}

{/* <StaffAllocation allStaffs={allstaff} /> */}
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="outlined"


+ 1
- 4
src/components/CreateTeam/StaffAllocation.tsx Bestand weergeven

@@ -138,7 +138,7 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
{ label: t("Staff Name"), name: "name" },
{ label: t("Position"), name: "currentPosition" },
{
label: t("Team Lead"),
label: t("teamLead"),
name: "action",
onClick: setTeamLead,
buttonIcon: <StarsIcon />,
@@ -194,9 +194,6 @@ const StaffAllocation: React.FC<Props> = ({ allStaffs: staff }) => {
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
>
<Stack gap={2}>
<Typography variant="overline" display="block">
{t("staff")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />


+ 41
- 38
src/components/EditSkill/EditSkill.tsx Bestand weergeven

@@ -20,7 +20,7 @@ import {
useForm,
useFormContext,
} from "react-hook-form";
import { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useRouter, useSearchParams } from "next/navigation";
import { Check, Close, Error, RestartAlt } from "@mui/icons-material";
@@ -39,10 +39,10 @@ const EditSkill: React.FC<Props> = async ({ skills }) => {
const router = useRouter();
const searchParams = useSearchParams();
const id = parseInt(searchParams.get("id") || "0");
const [tabIndex, setTabIndex] = useState(0);
const [filteredSkill, setFilteredSkill] = useState<SkillResult>(() =>
skills.filter((s) => s.id === id)[0] as SkillResult
);
// const [tabIndex, setTabIndex] = useState(0);
// const [filteredSkill, setFilteredSkill] = useState<SkillResult>(
// () => skills.filter((s) => s.id === id)[0] as SkillResult
// );
const errors = formProps.formState.errors;

const onSubmit = useCallback<SubmitHandler<CreateSkillInputs>>(
@@ -50,11 +50,11 @@ const EditSkill: React.FC<Props> = async ({ skills }) => {
try {
console.log(data);
const postData = {
...data,
id: id
}
await saveSkill(postData)
router.replace(`/settings/skill`)
...data,
id: id,
};
await saveSkill(postData);
router.replace(`/settings/skill`);
} catch (e) {
console.log(e);
setServerError(t("An error has occurred. Please try again later."));
@@ -67,33 +67,36 @@ const EditSkill: React.FC<Props> = async ({ skills }) => {
router.back();
};

const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
(_e, newValue) => {
setTabIndex(newValue);
},
[]
);
// const handleTabChange = useCallback<NonNullable<TabsProps["onChange"]>>(
// (_e, newValue) => {
// setTabIndex(newValue);
// },
// []
// );

useEffect(() => {
// const hasErrorsInTab = (
// tabIndex: number,
// errors: FieldErrors<CreateSkillInputs>
// ) => {
// switch (tabIndex) {
// case 0:
// return Object.keys(errors).length > 0;
// default:
// false;
// }
// };
const resetSkill = React.useCallback(() => {
formProps.reset({
name: filteredSkill.name,
code: filteredSkill.code,
description: filteredSkill.description
});
name: skills[0].name,
code: skills[0].code,
description: skills[0].description,
});
}, []);
useEffect(() => {
resetSkill()
}, [skills]);

const hasErrorsInTab = (
tabIndex: number,
errors: FieldErrors<CreateSkillInputs>
) => {
switch (tabIndex) {
case 0:
return Object.keys(errors).length > 0;
default:
false;
}
};

return (
<>
{serverError && (
@@ -107,7 +110,7 @@ const EditSkill: React.FC<Props> = async ({ skills }) => {
component="form"
onSubmit={formProps.handleSubmit(onSubmit)}
>
<Tabs
{/* <Tabs
value={tabIndex}
onChange={handleTabChange}
variant="scrollable"
@@ -121,14 +124,14 @@ const EditSkill: React.FC<Props> = async ({ skills }) => {
}
iconPosition="end"
/>
{/* <Tab label={t("Certification")} iconPosition="end" /> */}
</Tabs>
{tabIndex === 0 && <EditSkillForm />}
</Tabs> */}
{/* {tabIndex === 0 && <EditSkillForm />} */}
<EditSkillForm />
<Stack direction="row" justifyContent="flex-end" gap={1}>
<Button
variant="text"
startIcon={<RestartAlt />}
// onClick={() => console.log("asdasd")}
onClick={resetSkill}
>
{t("Reset")}
</Button>


+ 26
- 42
src/components/EditSkill/EditSkillForm.tsx Bestand weergeven

@@ -1,47 +1,30 @@
"use client";

import { CreateSkillInputs } from "@/app/api/skill/actions";
import {
Box,
Button,
Card,
CardContent,
Grid,
Stack,
Tab,
Tabs,
TabsProps,
TextField,
Typography,
} from "@mui/material";
import { useSearchParams } from "next/navigation";
import {
FieldErrors,
FormProvider,
SubmitErrorHandler,
SubmitHandler,
useForm,
useFormContext,
} from "react-hook-form";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import Typography from "@mui/material/Typography";
import { useTranslation } from "react-i18next";
import CardActions from "@mui/material/CardActions";
import RestartAlt from "@mui/icons-material/RestartAlt";
import Button from "@mui/material/Button";
import { Controller, useFormContext } from "react-hook-form";
import { FormControl, InputLabel, MenuItem, Select } from "@mui/material";
import { useCallback } from "react";
import { CreateSkillInputs } from "@/app/api/skill/actions";

interface Props {
// users: UserResult[]
}

const EditSkillForm: React.FC<Props> = async ({}) => {
const EditSkillForm: React.FC = () => {
const { t } = useTranslation();
const searchParams = useSearchParams();
const idString = searchParams.get("id");
const {
register,
setValue,
getValues,
formState: { errors, defaultValues },
control,
reset,
resetField,
setValue,
} = useFormContext<CreateSkillInputs>();
// const formProps = useForm({});

return (
<>
@@ -65,13 +48,14 @@ const EditSkillForm: React.FC<Props> = async ({}) => {
Boolean(errors.name) &&
(errors.name?.message
? t(errors.name.message)
: t("Please input correct name"))
}
: `${t("Please input correct ")}${t("name")}`

)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label={t("Skill Code")}
label={t("Code")}
fullWidth
rows={4}
{...register("code", {
@@ -82,13 +66,13 @@ const EditSkillForm: React.FC<Props> = async ({}) => {
Boolean(errors.code) &&
(errors.code?.message
? t(errors.code.message)
: t("Please input correct name"))
}
: `${t("Please input correct ")}${t("code")}`
)}
/>
</Grid>
<Grid item xs={12}>
<TextField
label={t("Skill Description")}
label={t("description")}
fullWidth
multiline
rows={4}
@@ -100,8 +84,8 @@ const EditSkillForm: React.FC<Props> = async ({}) => {
Boolean(errors.description) &&
(errors.description?.message
? t(errors.description.message)
: t("Please input correct name"))
}
: `${t("Please input correct ")}${t("description")}`
)}
/>
</Grid>
</Grid>


+ 6
- 9
src/components/EditTeam/Allocation.tsx Bestand weergeven

@@ -127,14 +127,14 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
const StaffPoolColumns = useMemo<Column<StaffResult>[]>(
() => [
{
label: t("Add"),
label: t("add"),
name: "id",
onClick: addStaff,
buttonIcon: <PersonAdd />,
},
{ label: t("Staff Id"), name: "staffId" },
{ label: t("Staff Name"), name: "name" },
{ label: t("Current Position"), name: "currentPosition" },
{ label: t("Position"), name: "currentPosition" },
],
[addStaff, t]
);
@@ -142,16 +142,16 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
const allocatedStaffColumns = useMemo<Column<StaffResult>[]>(
() => [
{
label: t("Remove"),
label: t("remove"),
name: "action",
onClick: removeStaff,
buttonIcon: <PersonRemove />,
},
{ label: t("Staff Id"), name: "staffId" },
{ label: t("Staff Name"), name: "name" },
{ label: t("Current Position"), name: "currentPosition" },
{ label: t("Position"), name: "currentPosition" },
{
label: t("Team Lead"),
label: t("teamLead"),
name: "action",
onClick: setTeamLead,
buttonIcon: <StarsIcon />,
@@ -210,9 +210,6 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
sx={{ display: "flex", flexDirection: "column", gap: 1 }}
>
<Stack gap={2}>
<Typography variant="overline" display="block">
{t("staff")}
</Typography>
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}>
<Grid item xs={6} display="flex" alignItems="center">
<Search sx={{ marginInlineEnd: 1 }} />
@@ -221,7 +218,7 @@ const Allocation: React.FC<Props> = ({ allStaffs: staff, teamLead }) => {
fullWidth
onChange={onQueryInputChange}
value={query}
placeholder={t("Search by staff ID, name or position.")}
placeholder={t("Search by Staff Id, Name or Position.")}
InputProps={{
endAdornment: query && (
<InputAdornment position="end">


+ 6
- 8
src/components/EditTeam/EditTeam.tsx Bestand weergeven

@@ -68,15 +68,13 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => {
);
useEffect(() => {
let idList: number[] = []
console.log(desc)
// console.log(desc)
if (idString) {
const filteredTeam = staff.filter(
(item) => {
console.log(item)
console.log(parseInt(idString))
return (item.teamId === parseInt(idString))}
);
console.log(filteredTeam)
// console.log(filteredTeam)
const tempDesc = desc.filter(
(item) => item.id === parseInt(idString)
)
@@ -100,15 +98,15 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => {
// }

idList = filteredIds
console.log(filteredIds)
// console.log(filteredIds)
}
console.log(idList)
// console.log(idList)
setFilteredItems(filteredTeam);
formProps.reset({description: tempDesc[0].description, addStaffIds: idList})
setFilteredDesc(tempDesc[0].description)
setFilteredName(tempDesc[0].name)
}
console.log(staff)
// console.log(staff)

setAllStaffs(staff)
@@ -133,7 +131,7 @@ const EditTeam: React.FC<Props> = async ({ staff, desc }) => {
const onSubmit = useCallback<SubmitHandler<CreateTeamInputs>>(
async (data) => {
try {
console.log(data);
// console.log(data);
const tempData = {
description: data.description,
addStaffIds: data.addStaffIds,


+ 106
- 0
src/components/PastEntryCalendar/PastEntryCalendar.tsx Bestand weergeven

@@ -0,0 +1,106 @@
import React from "react";
import {
RecordTimesheetInput,
RecordLeaveInput,
} from "@/app/api/timesheets/actions";
import {
DateCalendar,
LocalizationProvider,
PickersDay,
PickersDayProps,
} from "@mui/x-date-pickers";
import { useTranslation } from "react-i18next";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/zh-hk";
import timezone from "dayjs/plugin/timezone";
import utc from "dayjs/plugin/utc";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";

dayjs.extend(utc);
dayjs.extend(timezone);

dayjs.tz.guess();

export interface Props {
timesheet: RecordTimesheetInput;
leaves: RecordLeaveInput;
onDateSelect: (date: string) => void;
}

const getColor = (
hasTimeInput: boolean,
hasLeave: boolean,
): string | undefined => {
if (hasTimeInput && hasLeave) {
return "success.light";
} else if (hasTimeInput) {
return "info.light";
} else if (hasLeave) {
return "warning.light";
} else {
return undefined;
}
};

const EntryDay: React.FC<PickersDayProps<Dayjs> & Props> = ({
timesheet,
leaves,
...pickerProps
}) => {
const timesheetDays = Object.keys(timesheet);
const leaveDays = Object.keys(leaves);

const hasTimesheetInput = timesheetDays.some((day) =>
dayjs(day).isSame(pickerProps.day, "day"),
);

const hasLeaveInput = leaveDays.some((day) =>
dayjs(day).isSame(pickerProps.day, "day"),
);

return (
<PickersDay
{...pickerProps}
disabled={!(hasTimesheetInput || hasLeaveInput)}
sx={{ backgroundColor: getColor(hasTimesheetInput, hasLeaveInput) }}
/>
);
};

const PastEntryCalendar: React.FC<Props> = ({
timesheet,
leaves,
onDateSelect,
}) => {
const {
i18n: { language },
} = useTranslation("home");

const onChange = (day: Dayjs) => {
onDateSelect(day.format(INPUT_DATE_FORMAT));
};

return (
<LocalizationProvider
dateAdapter={AdapterDayjs}
adapterLocale={`${language}-hk`}
>
<DateCalendar
onChange={onChange}
disableFuture
// eslint-disable-next-line @typescript-eslint/no-explicit-any
slots={{ day: EntryDay as any }}
slotProps={{
day: {
timesheet,
leaves,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any,
}}
/>
</LocalizationProvider>
);
};

export default PastEntryCalendar;

+ 99
- 0
src/components/PastEntryCalendar/PastEntryCalendarModal.tsx Bestand weergeven

@@ -0,0 +1,99 @@
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
Stack,
Typography,
styled,
} from "@mui/material";
import PastEntryCalendar, {
Props as PastEntryCalendarProps,
} from "./PastEntryCalendar";
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { ArrowBack } from "@mui/icons-material";

interface Props extends Omit<PastEntryCalendarProps, "onDateSelect"> {
open: boolean;
handleClose: () => void;
}

const Indicator = styled(Box)(() => ({
borderRadius: "50%",
width: "1rem",
height: "1rem",
}));

const PastEntryCalendarModal: React.FC<Props> = ({
handleClose,
open,
timesheet,
leaves,
}) => {
const { t } = useTranslation("home");

const [selectedDate, setSelectedDate] = useState("");

const clearDate = useCallback(() => {
setSelectedDate("");
}, []);

const onClose = useCallback(() => {
handleClose();
}, [handleClose]);

return (
<Dialog onClose={onClose} open={open}>
<DialogTitle>{t("Past Entries")}</DialogTitle>
<DialogContent>
{selectedDate ? (
<Box>{selectedDate}</Box>
) : (
<Box>
<Stack>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "info.light" }} />
<Typography variant="caption">
{t("Has timesheet entry")}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "warning.light" }} />
<Typography variant="caption">
{t("Has leave entry")}
</Typography>
</Box>
<Box display="flex" alignItems="center" gap={1}>
<Indicator sx={{ backgroundColor: "success.light" }} />
<Typography variant="caption">
{t("Has both timesheet and leave entry")}
</Typography>
</Box>
</Stack>
<PastEntryCalendar
timesheet={timesheet}
leaves={leaves}
onDateSelect={setSelectedDate}
/>
</Box>
)}
</DialogContent>
{selectedDate && (
<DialogActions>
<Button
variant="outlined"
startIcon={<ArrowBack />}
onClick={clearDate}
>
{t("Back")}
</Button>
</DialogActions>
)}
</Dialog>
);
};

export default PastEntryCalendarModal;

+ 1
- 0
src/components/PastEntryCalendar/index.ts Bestand weergeven

@@ -0,0 +1 @@
export { default } from "./PastEntryCalendar";

+ 9
- 1
src/components/ProjectSearch/ProjectSearch.tsx Bestand weergeven

@@ -46,6 +46,12 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
type: "select",
options: uniq(projects.map((project) => project.team)),
},
{
label: t("Status"),
paramName: "status",
type: "select",
options: uniq(projects.map((project) => project.status)),
},
],
[t, projectCategories, projects],
);
@@ -74,6 +80,7 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
{ name: "category", label: t("Project Category") },
{ name: "team", label: t("Team") },
{ name: "client", label: t("Client") },
{ name: "status", label: t("Status") },
],
[t, onProjectClick],
);
@@ -90,7 +97,8 @@ const ProjectSearch: React.FC<Props> = ({ projects, projectCategories }) => {
p.name.toLowerCase().includes(query.name.toLowerCase()) &&
(query.client === "All" || p.client === query.client) &&
(query.category === "All" || p.category === query.category) &&
(query.team === "All" || p.team === query.team),
(query.team === "All" || p.team === query.team) &&
(query.status === "All" || p.status === query.status),
),
);
}}


+ 111
- 89
src/components/Report/ReportSearchBox/SearchBox.tsx Bestand weergeven

@@ -113,113 +113,135 @@ function SearchBox<T extends string>({
onSearch(inputs);
};
const handleDownload = async () => {
//setIsLoading(true);

try {
const response = await fetch('/temp/AR01_Late Start Report.xlsx', {
const response = await fetch('/api/reports', {
method: 'POST',
headers: {
'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'Content-Type': 'application/json',
},
body: JSON.stringify({ projectId: '123' }), // Example payload
});
if (!response.ok) throw new Error('Network response was not ok.');
const data = await response.blob();
const reader = new FileReader();
reader.onload = (e) => {
if (e.target && e.target.result) {
const ab = e.target.result as ArrayBuffer;
const workbook = XLSX.read(ab, { type: 'array' });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
const url = window.URL.createObjectURL(data);
const a = document.createElement('a');
a.href = url;
a.download = "Project_Cash_Flow_Report.xlsx";
document.body.appendChild(a);
a.click();
a.remove();
} catch (error) {
console.error('Error downloading the file: ', error);
}
};
// const handleDownload = async () => {
// //setIsLoading(true);

// try {
// const response = await fetch('/temp/AR01_Late Start Report.xlsx', {
// headers: {
// 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
// },
// });
// if (!response.ok) throw new Error('Network response was not ok.');

// const data = await response.blob();
// const reader = new FileReader();
// reader.onload = (e) => {
// if (e.target && e.target.result) {
// const ab = e.target.result as ArrayBuffer;
// const workbook = XLSX.read(ab, { type: 'array' });
// const firstSheetName = workbook.SheetNames[0];
// const worksheet = workbook.Sheets[firstSheetName];
// Add the current date to cell C2
const cellAddress = 'C2';
const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD
const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD
XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress });
// // Add the current date to cell C2
// const cellAddress = 'C2';
// const date = new Date().toISOString().split('T')[0]; // Format YYYY-MM-DD
// const formattedDate = date.replace(/-/g, '/'); // Change format to YYYY/MM/DD
// XLSX.utils.sheet_add_aoa(worksheet, [[formattedDate]], { origin: cellAddress });

// Style for cell A1: Font size 16 and bold
if (worksheet['A1']) {
worksheet['A1'].s = {
font: {
bold: true,
sz: 16, // Font size 16
//name: 'Times New Roman' // Specify font
}
};
}
// // Style for cell A1: Font size 16 and bold
// if (worksheet['A1']) {
// worksheet['A1'].s = {
// font: {
// bold: true,
// sz: 16, // Font size 16
// //name: 'Times New Roman' // Specify font
// }
// };
// }

// Apply styles from A2 to A4 (bold)
['A2', 'A3', 'A4'].forEach(cell => {
if (worksheet[cell]) {
worksheet[cell].s = { font: { bold: true } };
}
});
// // Apply styles from A2 to A4 (bold)
// ['A2', 'A3', 'A4'].forEach(cell => {
// if (worksheet[cell]) {
// worksheet[cell].s = { font: { bold: true } };
// }
// });

// Formatting from A6 to J6
// Apply styles from A6 to J6 (bold, bottom border, center alignment)
for (let col = 0; col < 10; col++) { // Columns A to J
const cellRef = XLSX.utils.encode_col(col) + '6';
if (worksheet[cellRef]) {
worksheet[cellRef].s = {
font: { bold: true },
alignment: { horizontal: 'center' },
border: {
bottom: { style: 'thin', color: { auto: 1 } }
}
};
}
}
// // Formatting from A6 to J6
// // Apply styles from A6 to J6 (bold, bottom border, center alignment)
// for (let col = 0; col < 10; col++) { // Columns A to J
// const cellRef = XLSX.utils.encode_col(col) + '6';
// if (worksheet[cellRef]) {
// worksheet[cellRef].s = {
// font: { bold: true },
// alignment: { horizontal: 'center' },
// border: {
// bottom: { style: 'thin', color: { auto: 1 } }
// }
// };
// }
// }

const firstTableData = [
['Column1', 'Column2', 'Column3'], // Row 1
['Data1', 'Data2', 'Data3'], // Row 2
// ... more rows as needed
];
// Find the last row of the first table
let lastRowOfFirstTable = 6; // Starting row for data in the first table
while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) {
lastRowOfFirstTable++;
}
// const firstTableData = [
// ['Column1', 'Column2', 'Column3'], // Row 1
// ['Data1', 'Data2', 'Data3'], // Row 2
// // ... more rows as needed
// ];
// // Find the last row of the first table
// let lastRowOfFirstTable = 6; // Starting row for data in the first table
// while (worksheet[XLSX.utils.encode_cell({ c: 0, r: lastRowOfFirstTable })]) {
// lastRowOfFirstTable++;
// }
// Calculate the maximum length of content in each column and set column width
const colWidths: number[] = [];
// // Calculate the maximum length of content in each column and set column width
// const colWidths: number[] = [];

const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][];
jsonData.forEach((row: (string | number)[]) => {
row.forEach((cell: string | number, index: number) => {
const valueLength = cell.toString().length;
colWidths[index] = Math.max(colWidths[index] || 0, valueLength);
});
});
// const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "", blankrows: true }) as (string | number)[][];
// jsonData.forEach((row: (string | number)[]) => {
// row.forEach((cell: string | number, index: number) => {
// const valueLength = cell.toString().length;
// colWidths[index] = Math.max(colWidths[index] || 0, valueLength);
// });
// });

// Apply calculated widths to each column, skipping column A
worksheet['!cols'] = colWidths.map((width, index) => {
if (index === 0) {
return { wch: 8 }; // Set default or specific width for column A if needed
}
return { wch: width + 2 }; // Add padding to width
});
// // Apply calculated widths to each column, skipping column A
// worksheet['!cols'] = colWidths.map((width, index) => {
// if (index === 0) {
// return { wch: 8 }; // Set default or specific width for column A if needed
// }
// return { wch: width + 2 }; // Add padding to width
// });

// Format filename with date
const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD
const filename = `AR01_Late_Start_Report_${today}.xlsx`; // Append formatted date to the filename
// // Format filename with date
// const today = new Date().toISOString().split('T')[0].replace(/-/g, '_'); // Get current date and format as YYYY_MM_DD
// const filename = `AR01_Late_Start_Report_${today}.xlsx`; // Append formatted date to the filename

// Convert workbook back to XLSX file
XLSX.writeFile(workbook, filename);
} else {
throw new Error('Failed to load file');
}
};
reader.readAsArrayBuffer(data);
} catch (error) {
console.error('Error downloading the file: ', error);
}
// // Convert workbook back to XLSX file
// XLSX.writeFile(workbook, filename);
// } else {
// throw new Error('Failed to load file');
// }
// };
// reader.readAsArrayBuffer(data);
// } catch (error) {
// console.error('Error downloading the file: ', error);
// }

//setIsLoading(false);
};
// //setIsLoading(false);
// };
return (
<Card>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 1 }}>


+ 12
- 11
src/components/SkillSearch/SkillSearch.tsx Bestand weergeven

@@ -8,6 +8,7 @@ import DeleteIcon from "@mui/icons-material/Delete";
import { useRouter } from "next/navigation";
import { deleteDialog, successDialog } from "../Swal/CustomAlerts";
import { SkillResult } from "@/app/api/skill";
import { deleteSkill } from "@/app/api/skill/actions";

interface Props {
skill: SkillResult[];
@@ -29,7 +30,7 @@ const SkillSearch: React.FC<Props> = ({ skill }) => {
type: "text",
},
{
label: t("Skill code"),
label: t("Skill Code"),
paramName: "code",
type: "text",
},
@@ -47,11 +48,11 @@ const SkillSearch: React.FC<Props> = ({ skill }) => {
);

const deleteClick = useCallback((skill: SkillResult) => {
// deleteDialog(async () => {
// await deleteStaff(skill.id);
// successDialog("Delete Success", t);
// setFilteredSkill((prev) => prev.filter((obj) => obj.id !== skill.id));
// }, t);
deleteDialog(async () => {
await deleteSkill(skill.id);
successDialog("Delete Success", t);
// setFilteredSkill((prev) => prev.filter((obj) => obj.id !== skill.id));
}, t);
}, []);

const columns = useMemo<Column<SkillResult>[]>(
@@ -62,12 +63,12 @@ const SkillSearch: React.FC<Props> = ({ skill }) => {
onClick: onSkillClick,
buttonIcon: <EditNote />,
},
{ name: "name", label: t("Name") },
{ name: "code", label: t("Code") },
{ name: "description", label: t("Description") },
{ name: "name", label: t("name") },
{ name: "code", label: t("code") },
{ name: "description", label: t("description") },
{
name: "action",
label: t("Actions"),
name: "delete",
label: t("Delete"),
onClick: deleteClick,
buttonIcon: <DeleteIcon />,
color: "error",


+ 0
- 1
src/components/SkillSearch/SkillSearchWrapper.tsx Bestand weergeven

@@ -17,7 +17,6 @@ interface SubComponents {

const SkillSearchWrapper: React.FC & SubComponents = async () => {
const skill = await fetchSkill()
console.log(skill);

return <SkillSearch skill={skill} />;
};


+ 4
- 4
src/components/StaffSearch/StaffSearch.tsx Bestand weergeven

@@ -61,7 +61,7 @@ const StaffSearch: React.FC<Props> = ({ staff, abilities }) => {

const onStaffClick = useCallback(
(staff: StaffResult) => {
console.log(staff);
// console.log(staff);
const id = staff.id;
router.push(`/settings/staff/edit?id=${id}`);
},
@@ -70,8 +70,8 @@ const StaffSearch: React.FC<Props> = ({ staff, abilities }) => {

const onUserClick = useCallback(
(staff: StaffResult) => {
console.log(staff);
router.push(`/settings/staff/user?id=${staff.id}`);
// console.log(staff);
router.push(`/settings/staff/user?id=${staff.userId}`);
},
[router, t]
);
@@ -94,7 +94,7 @@ const StaffSearch: React.FC<Props> = ({ staff, abilities }) => {
},
{
name: "id",
label: t("Actions"),
label: t("Users"),
onClick: onUserClick,
buttonIcon: <Person />,
isHidden: ![MAINTAIN_USER, VIEW_USER].some((ability) => abilities.includes(ability)),


+ 4
- 0
src/components/StyledDataGrid/StyledDataGrid.tsx Bestand weergeven

@@ -28,6 +28,10 @@ const StyledDataGrid = styled(DataGrid)(({ theme }) => ({
borderRadius: 0,
maxHeight: 50,
},
"& .MuiAutocomplete-root .MuiFilledInput-root": {
borderRadius: 0,
maxHeight: 50,
},
}));

export default StyledDataGrid;

+ 16
- 12
src/components/TeamSearch/TeamSearch.tsx Bestand weergeven

@@ -21,21 +21,28 @@ const TeamSearch: React.FC<Props> = ({ team }) => {
const { t } = useTranslation();
const [filteredTeam, setFilteredTeam] = useState(team);
const router = useRouter();
// translation
const edit = t("edit")
const name = t("name")
const code = t("code")
const description = t("description")
const teamLead = t("teamLead")
const delete_t = t("delete")

const searchCriteria: Criterion<SearchParamNames>[] = useMemo(
() => [
{
label: t("Team Name"),
label: name,
paramName: "name",
type: "text",
},
{
label: t("Team Code"),
label: code,
paramName: "code",
type: "text",
},
{
label: t("Team Description"),
label: description,
paramName: "description",
type: "text",
},
@@ -55,10 +62,7 @@ const TeamSearch: React.FC<Props> = ({ team }) => {
const onDeleteClick = useCallback((team: TeamResult) => {
deleteDialog(async () => {
await deleteTeam(team.id);

successDialog(t("Delete Success"), t);

setFilteredTeam((prev) => prev.filter((obj) => obj.id !== team.id));
}, t);
}, []);

@@ -66,17 +70,17 @@ const TeamSearch: React.FC<Props> = ({ team }) => {
() => [
{
name: "action",
label: t("Edit"),
label: edit,
onClick: onTeamClick,
buttonIcon: <EditNote />,
},
{ name: "name", label: t("Name") },
{ name: "code", label: t("Code") },
{ name: "description", label: t("description") },
{ name: "staffName", label: t("TeamLead") },
{ name: "name", label: name },
{ name: "code", label: code },
{ name: "description", label: description },
{ name: "staffName", label: teamLead },
{
name: "action",
label: t("Delete"),
label: delete_t,
onClick: onDeleteClick,
buttonIcon: <DeleteIcon />,
color: "error"


+ 7
- 84
src/components/TimesheetTable/MobileTimesheetEntry.tsx Bestand weergeven

@@ -17,6 +17,7 @@ import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import TimesheetEditModal, {
Props as TimesheetEditModalProps,
} from "./TimesheetEditModal";
import TimeEntryCard from "./TimeEntryCard";

interface Props {
date: string;
@@ -119,91 +120,13 @@ const MobileTimesheetEntry: React.FC<Props> = ({
const task = project?.tasks.find((t) => t.id === entry.taskId);

return (
<Card
<TimeEntryCard
key={`${entry.id}-${index}`}
sx={{ marginInline: 1, overflow: "visible" }}
>
<CardContent
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
gap: 2,
"&:last-child": {
paddingBottom: 2,
},
}}
>
<Box
display="flex"
justifyContent="space-between"
alignItems="flex-start"
gap={2}
>
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{project
? `${project.code} - ${project.name}`
: t("Non-billable Task")}
</Typography>
{task && (
<Typography variant="body2" component="div">
{task.name}
</Typography>
)}
</Box>
<IconButton
size="small"
color="primary"
onClick={openEditModal(entry)}
>
<Edit />
</IconButton>
</Box>
<Box display="flex" gap={2}>
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{t("Hours")}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.inputHours || 0)}
</Typography>
</Box>
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{t("Other Hours")}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.otHours || 0)}
</Typography>
</Box>
</Box>
{entry.remark && (
<Box>
<Typography
variant="body2"
component="div"
fontWeight="bold"
>
{t("Remark")}
</Typography>
<Typography component="p">{entry.remark}</Typography>
</Box>
)}
</CardContent>
</Card>
project={project}
task={task}
entry={entry}
onEdit={openEditModal(entry)}
/>
);
})
) : (


+ 135
- 94
src/components/TimesheetTable/ProjectSelect.tsx Bestand weergeven

@@ -10,6 +10,7 @@ import {
import { AssignedProject, ProjectWithTasks } from "@/app/api/projects";
import { useTranslation } from "react-i18next";
import differenceBy from "lodash/differenceBy";
import { TFunction } from "i18next";

interface Props {
allProjects: ProjectWithTasks[];
@@ -18,119 +19,159 @@ interface Props {
onProjectSelect: (projectId: number | string) => void;
}

// const AutocompleteProjectSelect: React.FC<Props> = ({
// allProjects,
// assignedProjects,
// value,
// onProjectSelect,
// }) => {
// const { t } = useTranslation("home");
// const nonAssignedProjects = useMemo(() => {
// return differenceBy(allProjects, assignedProjects, "id");
// }, [allProjects, assignedProjects]);

// const options = useMemo(() => {
// return [
// {
// value: "",
// label: t("None"),
// group: "non-billable",
// },
// ...assignedProjects.map((p) => ({
// value: p.id,
// label: `${p.code} - ${p.name}`,
// group: "assigned",
// })),
// ...nonAssignedProjects.map((p) => ({
// value: p.id,
// label: `${p.code} - ${p.name}`,
// group: "non-assigned",
// })),
// ];
// }, [assignedProjects, nonAssignedProjects, t]);

// return (
// <Autocomplete
// disableClearable
// fullWidth
// groupBy={(option) => option.group}
// getOptionLabel={(option) => option.label}
// options={options}
// renderInput={(params) => <TextField {...params} />}
// />
// );
// };
const getGroupName = (t: TFunction, groupName: string): string => {
switch (groupName) {
case "non-billable":
return t("Non-billable");
case "assigned":
return t("Assigned Projects");
case "non-assigned":
return t("Non-assigned Projects");
default:
return t("Ungrouped");
}
};

const ProjectSelect: React.FC<Props> = ({
const AutocompleteProjectSelect: React.FC<Props> = ({
allProjects,
assignedProjects,
value,
onProjectSelect,
}) => {
const { t } = useTranslation("home");

const nonAssignedProjects = useMemo(() => {
return differenceBy(allProjects, assignedProjects, "id");
}, [allProjects, assignedProjects]);

const options = useMemo(() => {
return [
{
value: "",
label: t("None"),
group: "non-billable",
},
...assignedProjects.map((p) => ({
value: p.id,
label: `${p.code} - ${p.name}`,
group: "assigned",
})),
...nonAssignedProjects.map((p) => ({
value: p.id,
label: `${p.code} - ${p.name}`,
group: "non-assigned",
})),
];
}, [assignedProjects, nonAssignedProjects, t]);

const currentValue = options.find((o) => o.value === value) || options[0];

const onChange = useCallback(
(event: SelectChangeEvent<number>) => {
const newValue = event.target.value;
onProjectSelect(newValue);
(event: React.SyntheticEvent, newValue: { value: number | string }) => {
onProjectSelect(newValue.value);
},
[onProjectSelect],
);

return (
<Select
displayEmpty
value={value || ""}
<Autocomplete
noOptionsText={t("No projects")}
disableClearable
fullWidth
value={currentValue}
onChange={onChange}
sx={{ width: "100%" }}
MenuProps={{
slotProps: {
paper: {
sx: { maxHeight: 400 },
},
},
anchorOrigin: {
vertical: "bottom",
horizontal: "left",
},
transformOrigin: {
vertical: "top",
horizontal: "left",
},
groupBy={(option) => option.group}
getOptionLabel={(option) => option.label}
options={options}
renderGroup={(params) => (
<>
<ListSubheader key={params.key}>
{getGroupName(t, params.group)}
</ListSubheader>
{params.children}
</>
)}
renderOption={(params, option) => {
return (
<MenuItem {...params} key={option.value} value={option.value}>
{option.label}
</MenuItem>
);
}}
>
<ListSubheader>{t("Non-billable")}</ListSubheader>
<MenuItem value={""}>{t("None")}</MenuItem>
{assignedProjects.length > 0 && [
<ListSubheader key="assignedProjectsSubHeader">
{t("Assigned Projects")}
</ListSubheader>,
...assignedProjects.map((project) => (
<MenuItem
key={project.id}
value={project.id}
sx={{ whiteSpace: "wrap" }}
>{`${project.code} - ${project.name}`}</MenuItem>
)),
]}
{nonAssignedProjects.length > 0 && [
<ListSubheader key="nonAssignedProjectsSubHeader">
{t("Non-assigned Projects")}
</ListSubheader>,
...nonAssignedProjects.map((project) => (
<MenuItem
key={project.id}
value={project.id}
sx={{ whiteSpace: "wrap" }}
>{`${project.code} - ${project.name}`}</MenuItem>
)),
]}
</Select>
renderInput={(params) => <TextField {...params} />}
/>
);
};

export default ProjectSelect;
// const ProjectSelect: React.FC<Props> = ({
// allProjects,
// assignedProjects,
// value,
// onProjectSelect,
// }) => {
// const { t } = useTranslation("home");

// const nonAssignedProjects = useMemo(() => {
// return differenceBy(allProjects, assignedProjects, "id");
// }, [allProjects, assignedProjects]);

// const onChange = useCallback(
// (event: SelectChangeEvent<number>) => {
// const newValue = event.target.value;
// onProjectSelect(newValue);
// },
// [onProjectSelect],
// );

// return (
// <Select
// displayEmpty
// value={value || ""}
// onChange={onChange}
// sx={{ width: "100%" }}
// MenuProps={{
// slotProps: {
// paper: {
// sx: { maxHeight: 400 },
// },
// },
// anchorOrigin: {
// vertical: "bottom",
// horizontal: "left",
// },
// transformOrigin: {
// vertical: "top",
// horizontal: "left",
// },
// }}
// >
// <ListSubheader>{t("Non-billable")}</ListSubheader>
// <MenuItem value={""}>{t("None")}</MenuItem>
// {assignedProjects.length > 0 && [
// <ListSubheader key="assignedProjectsSubHeader">
// {t("Assigned Projects")}
// </ListSubheader>,
// ...assignedProjects.map((project) => (
// <MenuItem
// key={project.id}
// value={project.id}
// sx={{ whiteSpace: "wrap" }}
// >{`${project.code} - ${project.name}`}</MenuItem>
// )),
// ]}
// {nonAssignedProjects.length > 0 && [
// <ListSubheader key="nonAssignedProjectsSubHeader">
// {t("Non-assigned Projects")}
// </ListSubheader>,
// ...nonAssignedProjects.map((project) => (
// <MenuItem
// key={project.id}
// value={project.id}
// sx={{ whiteSpace: "wrap" }}
// >{`${project.code} - ${project.name}`}</MenuItem>
// )),
// ]}
// </Select>
// );
// };

export default AutocompleteProjectSelect;

+ 87
- 0
src/components/TimesheetTable/TimeEntryCard.tsx Bestand weergeven

@@ -0,0 +1,87 @@
import { ProjectWithTasks } from "@/app/api/projects";
import { Task } from "@/app/api/tasks";
import { TimeEntry } from "@/app/api/timesheets/actions";
import { manhourFormatter } from "@/app/utils/formatUtil";
import { Edit } from "@mui/icons-material";
import { Box, Card, CardContent, IconButton, Typography } from "@mui/material";
import React from "react";
import { useTranslation } from "react-i18next";

interface Props {
project?: ProjectWithTasks;
task?: Task;
entry: TimeEntry;
onEdit?: () => void;
}

const TimeEntryCard: React.FC<Props> = ({ project, task, entry, onEdit }) => {
const { t } = useTranslation("home");
return (
<Card sx={{ marginInline: 1, overflow: "visible" }}>
<CardContent
sx={{
padding: 2,
display: "flex",
flexDirection: "column",
gap: 2,
"&:last-child": {
paddingBottom: 2,
},
}}
>
<Box
display="flex"
justifyContent="space-between"
alignItems="flex-start"
gap={2}
>
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{project
? `${project.code} - ${project.name}`
: t("Non-billable Task")}
</Typography>
{task && (
<Typography variant="body2" component="div">
{task.name}
</Typography>
)}
</Box>
{onEdit && (
<IconButton size="small" color="primary" onClick={onEdit}>
<Edit />
</IconButton>
)}
</Box>
<Box display="flex" gap={2}>
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{t("Hours")}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.inputHours || 0)}
</Typography>
</Box>
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{t("Other Hours")}
</Typography>
<Typography component="p">
{manhourFormatter.format(entry.otHours || 0)}
</Typography>
</Box>
</Box>
{entry.remark && (
<Box>
<Typography variant="body2" component="div" fontWeight="bold">
{t("Remark")}
</Typography>
<Typography component="p">{entry.remark}</Typography>
</Box>
)}
</CardContent>
</Card>
);
};

export default TimeEntryCard;

+ 0
- 3
src/components/UserGroupSearch/UserGroupSearch.tsx Bestand weergeven

@@ -45,10 +45,7 @@ const UserGroupSearch: React.FC<Props> = ({ users }) => {
const onDeleteClick = useCallback((group: UserGroupResult) => {
deleteDialog(async () => {
await deleteGroup(group.id);

successDialog(t("Delete Success"), t);

setFilteredUser((prev) => prev.filter((obj) => obj.id !== group.id));
}, t);
}, []);



+ 0
- 3
src/components/UserSearch/UserSearch.tsx Bestand weergeven

@@ -44,10 +44,7 @@ const UserSearch: React.FC<Props> = ({ users }) => {
const onDeleteClick = useCallback((users: UserResult) => {
deleteDialog(async () => {
await deleteUser(users.id);

successDialog(t("Delete Success"), t);

setFilteredUser((prev) => prev.filter((obj) => obj.id !== users.id));
}, t);
}, []);



+ 3
- 16
src/components/UserWorkspacePage/ProjectGrid.tsx Bestand weergeven

@@ -56,9 +56,6 @@ const ProjectGrid: React.FC<Props> = ({ projects }) => {
)})`}</Typography>
</Box>
{/* Hours Allocated */}
<Typography variant="subtitle2" sx={{ marginBlockStart: 2 }}>
{t("Hours Allocated:")}
</Typography>
<Box
sx={{
display: "flex",
@@ -66,23 +63,13 @@ const ProjectGrid: React.FC<Props> = ({ projects }) => {
alignItems: "baseline",
}}
>
<Typography variant="caption">{t("Normal")}</Typography>
<Typography variant="subtitle2" sx={{ marginBlockStart: 2 }}>
{t("Hours Allocated:")}
</Typography>
<Typography>
{manhourFormatter.format(project.hoursAllocated)}
</Typography>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
}}
>
<Typography variant="caption">{t("(Others)")}</Typography>
<Typography>{`(${manhourFormatter.format(
project.hoursAllocatedOther,
)})`}</Typography>
</Box>
</CardContent>
</Card>
</Grid>


+ 27
- 8
src/components/UserWorkspacePage/UserWorkspacePage.tsx Bestand weergeven

@@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { Add } from "@mui/icons-material";
import { Typography } from "@mui/material";
import { Box, Typography } from "@mui/material";
import ButtonGroup from "@mui/material/ButtonGroup";
import AssignedProjects from "./AssignedProjects";
import TimesheetModal from "../TimesheetModal";
@@ -16,6 +16,8 @@ import {
} from "@/app/api/timesheets/actions";
import LeaveModal from "../LeaveModal";
import { LeaveType } from "@/app/api/timesheets";
import { CalendarIcon } from "@mui/x-date-pickers";
import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal";

export interface Props {
leaveTypes: LeaveType[];
@@ -36,6 +38,7 @@ const UserWorkspacePage: React.FC<Props> = ({
}) => {
const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false);
const [isLeaveModalVisible, setLeaveModalVisible] = useState(false);
const [isPastEventModalVisible, setPastEventModalVisible] = useState(false);
const { t } = useTranslation("home");

const handleAddTimesheetButtonClick = useCallback(() => {
@@ -54,6 +57,14 @@ const UserWorkspacePage: React.FC<Props> = ({
setLeaveModalVisible(false);
}, []);

const handlePastEventClick = useCallback(() => {
setPastEventModalVisible(true);
}, []);

const handlePastEventClose = useCallback(() => {
setPastEventModalVisible(false);
}, []);

return (
<>
<Stack
@@ -65,12 +76,14 @@ const UserWorkspacePage: React.FC<Props> = ({
<Typography variant="h4" marginInlineEnd={2}>
{t("User Workspace")}
</Typography>
<Stack
direction="row"
justifyContent="right"
flexWrap="wrap"
spacing={2}
>
<Box display="flex" flexWrap="wrap" gap={2}>
<Button
startIcon={<CalendarIcon />}
variant="outlined"
onClick={handlePastEventClick}
>
{t("View Past Entries")}
</Button>
<ButtonGroup variant="contained">
<Button startIcon={<Add />} onClick={handleAddTimesheetButtonClick}>
{t("Enter Time")}
@@ -79,8 +92,14 @@ const UserWorkspacePage: React.FC<Props> = ({
{t("Record Leave")}
</Button>
</ButtonGroup>
</Stack>
</Box>
</Stack>
<PastEntryCalendarModal
open={isPastEventModalVisible}
handleClose={handlePastEventClose}
timesheet={defaultTimesheets}
leaves={defaultLeaveRecords}
/>
<TimesheetModal
isOpen={isTimeheetModalVisible}
onClose={handleCloseTimesheetModal}


+ 18
- 0
src/i18n/zh/skill.json Bestand weergeven

@@ -0,0 +1,18 @@
{
"Actions": "編輯",
"Skill": "技能",
"Skill Name": "名稱",
"Skill Code": "編號",
"Skill Info": "技能詳情",
"name": "技能",
"code": "編號",
"description": "描述",
"Confirm": "確定",
"Cancel": "取消",
"Delete": "刪除",
"Reset": "重設",
"Edit Skill": "編輯技能",
"Create Skill": "建立技能",
"Please input correct ": "請輸入正確"

}

+ 1
- 0
src/i18n/zh/staff.json Bestand weergeven

@@ -6,6 +6,7 @@
"Grade": "級別",
"Current Position": "現職",
"Actions": "編輯",
"Users": "用戶",
"Create Staff": "新增員工",
"Company": "公司",
"Department": "部門",


+ 25
- 0
src/i18n/zh/team.json Bestand weergeven

@@ -0,0 +1,25 @@
{
"Team": "隊伍",
"Team Info": "隊伍資料",
"Staff Allocation": "員工分配",
"Create Team": "建立隊伍",
"Edit Team": "編輯隊伍",
"name": "名稱",
"code": "編號",
"description": "描述",
"Team Description": "隊伍描述",
"edit": "編輯",
"teamLead": "負責人",
"Confirm": "確定",
"Cancel": "取消",
"delete": "刪除",
"add": "加入",
"remove": "移除",
"Staff Pool": "可分配員工",
"Allocated Staff": "已分配員工",
"Staff Id": "員工編號",
"Staff Name": "員工名稱",
"Position": "職位",
"Search by Staff Id, Name or Position.": "按員工編號、姓名或職位搜索",
"An error has occurred. Please try again later.": "發生了錯誤。請稍後再試"
}

+ 10
- 0
src/theme/devias-material-kit/components.ts Bestand weergeven

@@ -172,6 +172,16 @@ const components: ThemeOptions["components"] = {
},
},
},
MuiAutocomplete: {
styleOverrides: {
root: {
"& .MuiFilledInput-root": {
paddingTop: 8,
paddingBottom: 8,
},
},
},
},
MuiFilledInput: {
styleOverrides: {
root: {


Laden…
Annuleren
Opslaan