diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx index b93ed10..98a0800 100644 --- a/src/app/(main)/layout.tsx +++ b/src/app/(main)/layout.tsx @@ -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" }, }} > - - - {children} - + + + + {children} + + ); diff --git a/src/app/(main)/settings/skill/create/page.tsx b/src/app/(main)/settings/skill/create/page.tsx index c98f993..c912af3 100644 --- a/src/app/(main)/settings/skill/create/page.tsx +++ b/src/app/(main)/settings/skill/create/page.tsx @@ -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 ( <> diff --git a/src/app/(main)/settings/skill/edit/page.tsx b/src/app/(main)/settings/skill/edit/page.tsx index 05ff61f..7834b4f 100644 --- a/src/app/(main)/settings/skill/edit/page.tsx +++ b/src/app/(main)/settings/skill/edit/page.tsx @@ -17,13 +17,13 @@ const EditSkillPage: React.FC = async ({ searchParams, }) => { console.log(searchParams.id) - const { t } = await getServerI18n("staff"); + const { t } = await getServerI18n("skill"); return ( <> {t("Edit Skill")} - + }> diff --git a/src/app/(main)/settings/skill/page.tsx b/src/app/(main)/settings/skill/page.tsx index f263c87..bcb14ab 100644 --- a/src/app/(main)/settings/skill/page.tsx +++ b/src/app/(main)/settings/skill/page.tsx @@ -38,7 +38,7 @@ const Skill: React.FC = async () => { {t("Create Skill")} - + }> diff --git a/src/app/(main)/settings/team/create/page.tsx b/src/app/(main)/settings/team/create/page.tsx index a47d81c..f748270 100644 --- a/src/app/(main)/settings/team/create/page.tsx +++ b/src/app/(main)/settings/team/create/page.tsx @@ -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 ( <> {t("Create Team")} - + diff --git a/src/app/(main)/settings/team/page.tsx b/src/app/(main)/settings/team/page.tsx index 5e78fb3..b2b67ab 100644 --- a/src/app/(main)/settings/team/page.tsx +++ b/src/app/(main)/settings/team/page.tsx @@ -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")} - + }> diff --git a/src/app/api/customer/index.ts b/src/app/api/customer/index.ts index 7ed4359..ed4d947 100644 --- a/src/app/api/customer/index.ts +++ b/src/app/api/customer/index.ts @@ -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 { diff --git a/src/app/api/group/actions.ts b/src/app/api/group/actions.ts index 1122c16..eadfe7d 100644 --- a/src/app/api/group/actions.ts +++ b/src/app/api/group/actions.ts @@ -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 }; \ No newline at end of file diff --git a/src/app/api/projects/actions.ts b/src/app/api/projects/actions.ts index c80dff0..c1be476 100644 --- a/src/app/api/projects/actions.ts +++ b/src/app/api/projects/actions.ts @@ -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; diff --git a/src/app/api/projects/index.ts b/src/app/api/projects/index.ts index 30bd385..90d1f4a 100644 --- a/src/app/api/projects/index.ts +++ b/src/app/api/projects/index.ts @@ -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 = () => { diff --git a/src/app/api/reports/actions.ts b/src/app/api/reports/actions.ts index 1c4a175..b06b66b 100644 --- a/src/app/api/reports/actions.ts +++ b/src/app/api/reports/actions.ts @@ -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( + `${BASE_API_URL}/reports/downloadLateStartReport`, + { + method: "POST", + body: JSON.stringify(data), + headers: { "Content-Type": "application/json" }, + }, + ); + return reportBlob }; \ No newline at end of file diff --git a/src/app/api/reports/index.ts b/src/app/api/reports/index.ts index 8923829..1cea414 100644 --- a/src/app/api/reports/index.ts +++ b/src/app/api/reports/index.ts @@ -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; +} diff --git a/src/app/api/skill/actions.ts b/src/app/api/skill/actions.ts index 6a0deca..15a27a7 100644 --- a/src/app/api/skill/actions.ts +++ b/src/app/api/skill/actions.ts @@ -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 }; \ No newline at end of file diff --git a/src/app/api/skill/index.ts b/src/app/api/skill/index.ts index a235426..0ff8d1c 100644 --- a/src/app/api/skill/index.ts +++ b/src/app/api/skill/index.ts @@ -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(`${BASE_API_URL}/skill`, { - next: { tags: ["sill"] }, + next: { tags: ["skill"] }, }); }); export const fetchSkillDetail = cache(async (id: number) => { return serverFetchJson(`${BASE_API_URL}/skill/${id}`, { - next: { tags: ["sill"] }, + next: { tags: ["skill"] }, }); }); \ No newline at end of file diff --git a/src/app/api/staff/actions.ts b/src/app/api/staff/actions.ts index 88375d0..fca9727 100644 --- a/src/app/api/staff/actions.ts +++ b/src/app/api/staff/actions.ts @@ -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 }; diff --git a/src/app/api/staff/index.ts b/src/app/api/staff/index.ts index f24f187..6b15f42 100644 --- a/src/app/api/staff/index.ts +++ b/src/app/api/staff/index.ts @@ -38,6 +38,7 @@ export interface StaffResult { data: data; teamId: number; staffName: string; + userId: number; } export interface searchInput { staffId: string; diff --git a/src/app/api/subsidiary/index.ts b/src/app/api/subsidiary/index.ts index c83f4db..818bebf 100644 --- a/src/app/api/subsidiary/index.ts +++ b/src/app/api/subsidiary/index.ts @@ -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 { diff --git a/src/app/api/team/actions.ts b/src/app/api/team/actions.ts index 47e1a82..f57a17a 100644 --- a/src/app/api/team/actions.ts +++ b/src/app/api/team/actions.ts @@ -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 }; diff --git a/src/app/api/user/actions.ts b/src/app/api/user/actions.ts index 919634c..b6329a8 100644 --- a/src/app/api/user/actions.ts +++ b/src/app/api/user/actions.ts @@ -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) => { diff --git a/src/app/utils/fetchUtil.ts b/src/app/utils/fetchUtil.ts index c1f310b..5e4533e 100644 --- a/src/app/utils/fetchUtil.ts +++ b/src/app/utils/fetchUtil.ts @@ -38,18 +38,23 @@ type FetchParams = Parameters; export async function serverFetchJson(...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 ); } } diff --git a/src/app/utils/formatUtil.ts b/src/app/utils/formatUtil.ts index 4d773e9..150b350 100644 --- a/src/app/utils/formatUtil.ts +++ b/src/app/utils/formatUtil.ts @@ -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}`; -} \ No newline at end of file +} diff --git a/src/components/AppBar/Profile.tsx b/src/components/AppBar/Profile.tsx index 7b48190..9fbe8e5 100644 --- a/src/components/AppBar/Profile.tsx +++ b/src/components/AppBar/Profile.tsx @@ -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; @@ -26,8 +26,17 @@ const Profile: React.FC = ({ 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 = ({ avatarImageSrc, profileName }) => { {profileName} - {router.replace("/settings/changepassword")}}>{t("Change Password")} + { router.replace("/settings/changepassword") }}>{t("Change Password")} + {language === "zh" && { onLangClick("en") }}>{t("Change To English Version")}} + {language === "en" && { onLangClick("zh") }}>{t("Change To Chinese Version")}} signOut()}>{t("Sign out")} diff --git a/src/components/Breadcrumb/Breadcrumb.tsx b/src/components/Breadcrumb/Breadcrumb.tsx index 1679019..516aca2 100644 --- a/src/components/Breadcrumb/Breadcrumb.tsx +++ b/src/components/Breadcrumb/Breadcrumb.tsx @@ -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 ( - - - - {segments.map((segment, index) => { - const href = segments.slice(0, index + 1).join("/"); - const label = pathToLabelMap[href] || segment; + + + {segments.map((segment, index) => { + const href = segments.slice(0, index + 1).join("/"); + const label = pathToLabelMap[href] || segment; - if (index === segments.length - 1) { - return ( - - {label} - {/* {t(label)} */} - - ); - } else { - return ( - - {label} - - ); - } - })} - - - - - - + if (index === segments.length - 1) { + return ( + + {label} + {/* {t(label)} */} + + ); + } else { + return ( + + {label} + + ); + } + })} + + + + + ); }; diff --git a/src/components/Breadcrumb/Clock.tsx b/src/components/Breadcrumb/Clock.tsx index 8ddf42b..9e34340 100644 --- a/src/components/Breadcrumb/Clock.tsx +++ b/src/components/Breadcrumb/Clock.tsx @@ -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 = (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 ( - - {formattedDateTime} - - ); + return ( + + + {clockTimeFormatter(language).format(currentDateTime)} + + + ); }; export default Clock; diff --git a/src/components/ChangePassword/ChangePassword.tsx b/src/components/ChangePassword/ChangePassword.tsx index 1fc384f..01dedcb 100644 --- a/src/components/ChangePassword/ChangePassword.tsx +++ b/src/components/ChangePassword/ChangePassword.tsx @@ -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[] diff --git a/src/components/CreateProject/CreateProject.tsx b/src/components/CreateProject/CreateProject.tsx index c4aa9d5..4242afb 100644 --- a/src/components/CreateProject/CreateProject.tsx +++ b/src/components/CreateProject/CreateProject.tsx @@ -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 = ({ 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 = ({ 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 = ({ allocatedStaffIds: [], milestones: {}, totalManhour: 0, + taskTemplateId: "All", ...defaultInputs, // manhourPercentageByGrade should have a sensible default @@ -289,7 +292,8 @@ const CreateProject: React.FC = ({ > {isEditMode && !(formProps.getValues("projectDeleted") === true) && ( - {!formProps.getValues("projectActualStart") && ( + {/* {!formProps.getValues("projectActualStart") && ( */} + {formProps.getValues("projectStatus") === "Pending to Start" && ( )} - {formProps.getValues("projectActualStart") && - !formProps.getValues("projectActualEnd") && ( + {/* {formProps.getValues("projectActualStart") && + !formProps.getValues("projectActualEnd") && ( */} + {formProps.getValues("projectStatus") === "On-going" && ( )} {!( - formProps.getValues("projectActualStart") && - formProps.getValues("projectActualEnd") + // formProps.getValues("projectActualStart") && + // formProps.getValues("projectActualEnd") + formProps.getValues("projectStatus") === "Completed" || + formProps.getValues("projectStatus") === "Deleted" ) && ( diff --git a/src/components/EditSkill/EditSkillForm.tsx b/src/components/EditSkill/EditSkillForm.tsx index 120d2e5..50f813b 100644 --- a/src/components/EditSkill/EditSkillForm.tsx +++ b/src/components/EditSkill/EditSkillForm.tsx @@ -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 = 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(); - // const formProps = useForm({}); return ( <> @@ -65,13 +48,14 @@ const EditSkillForm: React.FC = async ({}) => { Boolean(errors.name) && (errors.name?.message ? t(errors.name.message) - : t("Please input correct name")) - } + : `${t("Please input correct ")}${t("name")}` + + )} /> = async ({}) => { Boolean(errors.code) && (errors.code?.message ? t(errors.code.message) - : t("Please input correct name")) - } + : `${t("Please input correct ")}${t("code")}` + )} /> = async ({}) => { Boolean(errors.description) && (errors.description?.message ? t(errors.description.message) - : t("Please input correct name")) - } + : `${t("Please input correct ")}${t("description")}` + )} /> diff --git a/src/components/EditTeam/Allocation.tsx b/src/components/EditTeam/Allocation.tsx index b732168..2376ece 100644 --- a/src/components/EditTeam/Allocation.tsx +++ b/src/components/EditTeam/Allocation.tsx @@ -127,14 +127,14 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { const StaffPoolColumns = useMemo[]>( () => [ { - label: t("Add"), + label: t("add"), name: "id", onClick: addStaff, buttonIcon: , }, { 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 = ({ allStaffs: staff, teamLead }) => { const allocatedStaffColumns = useMemo[]>( () => [ { - label: t("Remove"), + label: t("remove"), name: "action", onClick: removeStaff, buttonIcon: , }, { 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: , @@ -210,9 +210,6 @@ const Allocation: React.FC = ({ allStaffs: staff, teamLead }) => { sx={{ display: "flex", flexDirection: "column", gap: 1 }} > - - {t("staff")} - @@ -221,7 +218,7 @@ const Allocation: React.FC = ({ 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 && ( diff --git a/src/components/EditTeam/EditTeam.tsx b/src/components/EditTeam/EditTeam.tsx index 432bc0e..cd5f83a 100644 --- a/src/components/EditTeam/EditTeam.tsx +++ b/src/components/EditTeam/EditTeam.tsx @@ -68,15 +68,13 @@ const EditTeam: React.FC = 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 = 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 = async ({ staff, desc }) => { const onSubmit = useCallback>( async (data) => { try { - console.log(data); + // console.log(data); const tempData = { description: data.description, addStaffIds: data.addStaffIds, diff --git a/src/components/PastEntryCalendar/PastEntryCalendar.tsx b/src/components/PastEntryCalendar/PastEntryCalendar.tsx new file mode 100644 index 0000000..a9e6a14 --- /dev/null +++ b/src/components/PastEntryCalendar/PastEntryCalendar.tsx @@ -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 & 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 ( + + ); +}; + +const PastEntryCalendar: React.FC = ({ + timesheet, + leaves, + onDateSelect, +}) => { + const { + i18n: { language }, + } = useTranslation("home"); + + const onChange = (day: Dayjs) => { + onDateSelect(day.format(INPUT_DATE_FORMAT)); + }; + + return ( + + + + ); +}; + +export default PastEntryCalendar; diff --git a/src/components/PastEntryCalendar/PastEntryCalendarModal.tsx b/src/components/PastEntryCalendar/PastEntryCalendarModal.tsx new file mode 100644 index 0000000..53fd8dc --- /dev/null +++ b/src/components/PastEntryCalendar/PastEntryCalendarModal.tsx @@ -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 { + open: boolean; + handleClose: () => void; +} + +const Indicator = styled(Box)(() => ({ + borderRadius: "50%", + width: "1rem", + height: "1rem", +})); + +const PastEntryCalendarModal: React.FC = ({ + handleClose, + open, + timesheet, + leaves, +}) => { + const { t } = useTranslation("home"); + + const [selectedDate, setSelectedDate] = useState(""); + + const clearDate = useCallback(() => { + setSelectedDate(""); + }, []); + + const onClose = useCallback(() => { + handleClose(); + }, [handleClose]); + + return ( + + {t("Past Entries")} + + {selectedDate ? ( + {selectedDate} + ) : ( + + + + + + {t("Has timesheet entry")} + + + + + + {t("Has leave entry")} + + + + + + {t("Has both timesheet and leave entry")} + + + + + + )} + + {selectedDate && ( + + + + )} + + ); +}; + +export default PastEntryCalendarModal; diff --git a/src/components/PastEntryCalendar/index.ts b/src/components/PastEntryCalendar/index.ts new file mode 100644 index 0000000..1c7ee56 --- /dev/null +++ b/src/components/PastEntryCalendar/index.ts @@ -0,0 +1 @@ +export { default } from "./PastEntryCalendar"; diff --git a/src/components/ProjectSearch/ProjectSearch.tsx b/src/components/ProjectSearch/ProjectSearch.tsx index 55b4cc7..79ee51b 100644 --- a/src/components/ProjectSearch/ProjectSearch.tsx +++ b/src/components/ProjectSearch/ProjectSearch.tsx @@ -46,6 +46,12 @@ const ProjectSearch: React.FC = ({ 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 = ({ 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 = ({ 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), ), ); }} diff --git a/src/components/Report/ReportSearchBox/SearchBox.tsx b/src/components/Report/ReportSearchBox/SearchBox.tsx index 463aa0c..637d6f2 100644 --- a/src/components/Report/ReportSearchBox/SearchBox.tsx +++ b/src/components/Report/ReportSearchBox/SearchBox.tsx @@ -113,113 +113,135 @@ function SearchBox({ 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 ( diff --git a/src/components/SkillSearch/SkillSearch.tsx b/src/components/SkillSearch/SkillSearch.tsx index c13ea19..9642827 100644 --- a/src/components/SkillSearch/SkillSearch.tsx +++ b/src/components/SkillSearch/SkillSearch.tsx @@ -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 = ({ skill }) => { type: "text", }, { - label: t("Skill code"), + label: t("Skill Code"), paramName: "code", type: "text", }, @@ -47,11 +48,11 @@ const SkillSearch: React.FC = ({ 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[]>( @@ -62,12 +63,12 @@ const SkillSearch: React.FC = ({ skill }) => { onClick: onSkillClick, buttonIcon: , }, - { 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: , color: "error", diff --git a/src/components/SkillSearch/SkillSearchWrapper.tsx b/src/components/SkillSearch/SkillSearchWrapper.tsx index 33d0547..0f721d1 100644 --- a/src/components/SkillSearch/SkillSearchWrapper.tsx +++ b/src/components/SkillSearch/SkillSearchWrapper.tsx @@ -17,7 +17,6 @@ interface SubComponents { const SkillSearchWrapper: React.FC & SubComponents = async () => { const skill = await fetchSkill() - console.log(skill); return ; }; diff --git a/src/components/StaffSearch/StaffSearch.tsx b/src/components/StaffSearch/StaffSearch.tsx index e19e915..7d2db97 100644 --- a/src/components/StaffSearch/StaffSearch.tsx +++ b/src/components/StaffSearch/StaffSearch.tsx @@ -61,7 +61,7 @@ const StaffSearch: React.FC = ({ 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 = ({ 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 = ({ staff, abilities }) => { }, { name: "id", - label: t("Actions"), + label: t("Users"), onClick: onUserClick, buttonIcon: , isHidden: ![MAINTAIN_USER, VIEW_USER].some((ability) => abilities.includes(ability)), diff --git a/src/components/StyledDataGrid/StyledDataGrid.tsx b/src/components/StyledDataGrid/StyledDataGrid.tsx index 743d288..3ea0dce 100644 --- a/src/components/StyledDataGrid/StyledDataGrid.tsx +++ b/src/components/StyledDataGrid/StyledDataGrid.tsx @@ -28,6 +28,10 @@ const StyledDataGrid = styled(DataGrid)(({ theme }) => ({ borderRadius: 0, maxHeight: 50, }, + "& .MuiAutocomplete-root .MuiFilledInput-root": { + borderRadius: 0, + maxHeight: 50, + }, })); export default StyledDataGrid; diff --git a/src/components/TeamSearch/TeamSearch.tsx b/src/components/TeamSearch/TeamSearch.tsx index 71ecb79..a73acdb 100644 --- a/src/components/TeamSearch/TeamSearch.tsx +++ b/src/components/TeamSearch/TeamSearch.tsx @@ -21,21 +21,28 @@ const TeamSearch: React.FC = ({ 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[] = 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 = ({ 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 = ({ team }) => { () => [ { name: "action", - label: t("Edit"), + label: edit, onClick: onTeamClick, buttonIcon: , }, - { 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: , color: "error" diff --git a/src/components/TimesheetTable/MobileTimesheetEntry.tsx b/src/components/TimesheetTable/MobileTimesheetEntry.tsx index 9f709d8..03a0487 100644 --- a/src/components/TimesheetTable/MobileTimesheetEntry.tsx +++ b/src/components/TimesheetTable/MobileTimesheetEntry.tsx @@ -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 = ({ const task = project?.tasks.find((t) => t.id === entry.taskId); return ( - - - - - - {project - ? `${project.code} - ${project.name}` - : t("Non-billable Task")} - - {task && ( - - {task.name} - - )} - - - - - - - - - {t("Hours")} - - - {manhourFormatter.format(entry.inputHours || 0)} - - - - - {t("Other Hours")} - - - {manhourFormatter.format(entry.otHours || 0)} - - - - {entry.remark && ( - - - {t("Remark")} - - {entry.remark} - - )} - - + project={project} + task={task} + entry={entry} + onEdit={openEditModal(entry)} + /> ); }) ) : ( diff --git a/src/components/TimesheetTable/ProjectSelect.tsx b/src/components/TimesheetTable/ProjectSelect.tsx index 762512c..3c1ab51 100644 --- a/src/components/TimesheetTable/ProjectSelect.tsx +++ b/src/components/TimesheetTable/ProjectSelect.tsx @@ -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 = ({ -// 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 ( -// option.group} -// getOptionLabel={(option) => option.label} -// options={options} -// renderInput={(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 = ({ +const AutocompleteProjectSelect: React.FC = ({ 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) => { - const newValue = event.target.value; - onProjectSelect(newValue); + (event: React.SyntheticEvent, newValue: { value: number | string }) => { + onProjectSelect(newValue.value); }, [onProjectSelect], ); return ( - + renderInput={(params) => } + /> ); }; -export default ProjectSelect; +// const ProjectSelect: React.FC = ({ +// allProjects, +// assignedProjects, +// value, +// onProjectSelect, +// }) => { +// const { t } = useTranslation("home"); + +// const nonAssignedProjects = useMemo(() => { +// return differenceBy(allProjects, assignedProjects, "id"); +// }, [allProjects, assignedProjects]); + +// const onChange = useCallback( +// (event: SelectChangeEvent) => { +// const newValue = event.target.value; +// onProjectSelect(newValue); +// }, +// [onProjectSelect], +// ); + +// return ( +// +// ); +// }; + +export default AutocompleteProjectSelect; diff --git a/src/components/TimesheetTable/TimeEntryCard.tsx b/src/components/TimesheetTable/TimeEntryCard.tsx new file mode 100644 index 0000000..445e182 --- /dev/null +++ b/src/components/TimesheetTable/TimeEntryCard.tsx @@ -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 = ({ project, task, entry, onEdit }) => { + const { t } = useTranslation("home"); + return ( + + + + + + {project + ? `${project.code} - ${project.name}` + : t("Non-billable Task")} + + {task && ( + + {task.name} + + )} + + {onEdit && ( + + + + )} + + + + + {t("Hours")} + + + {manhourFormatter.format(entry.inputHours || 0)} + + + + + {t("Other Hours")} + + + {manhourFormatter.format(entry.otHours || 0)} + + + + {entry.remark && ( + + + {t("Remark")} + + {entry.remark} + + )} + + + ); +}; + +export default TimeEntryCard; diff --git a/src/components/UserGroupSearch/UserGroupSearch.tsx b/src/components/UserGroupSearch/UserGroupSearch.tsx index fee25e4..7a8f7dd 100644 --- a/src/components/UserGroupSearch/UserGroupSearch.tsx +++ b/src/components/UserGroupSearch/UserGroupSearch.tsx @@ -45,10 +45,7 @@ const UserGroupSearch: React.FC = ({ 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); }, []); diff --git a/src/components/UserSearch/UserSearch.tsx b/src/components/UserSearch/UserSearch.tsx index 658d25c..b7ac669 100644 --- a/src/components/UserSearch/UserSearch.tsx +++ b/src/components/UserSearch/UserSearch.tsx @@ -44,10 +44,7 @@ const UserSearch: React.FC = ({ 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); }, []); diff --git a/src/components/UserWorkspacePage/ProjectGrid.tsx b/src/components/UserWorkspacePage/ProjectGrid.tsx index ad44116..275d48d 100644 --- a/src/components/UserWorkspacePage/ProjectGrid.tsx +++ b/src/components/UserWorkspacePage/ProjectGrid.tsx @@ -56,9 +56,6 @@ const ProjectGrid: React.FC = ({ projects }) => { )})`} {/* Hours Allocated */} - - {t("Hours Allocated:")} - = ({ projects }) => { alignItems: "baseline", }} > - {t("Normal")} + + {t("Hours Allocated:")} + {manhourFormatter.format(project.hoursAllocated)} - - {t("(Others)")} - {`(${manhourFormatter.format( - project.hoursAllocatedOther, - )})`} - diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index 062e233..0c8615f 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -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 = ({ }) => { 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 = ({ setLeaveModalVisible(false); }, []); + const handlePastEventClick = useCallback(() => { + setPastEventModalVisible(true); + }, []); + + const handlePastEventClose = useCallback(() => { + setPastEventModalVisible(false); + }, []); + return ( <> = ({ {t("User Workspace")} - + + - + +