diff --git a/src/app/(main)/home/page.tsx b/src/app/(main)/home/page.tsx index bd0c08a..a83291d 100644 --- a/src/app/(main)/home/page.tsx +++ b/src/app/(main)/home/page.tsx @@ -12,6 +12,7 @@ import { fetchAssignedProjects, fetchProjectWithTasks, } from "@/app/api/projects"; +import { fetchHolidays } from "@/app/api/holidays"; export const metadata: Metadata = { title: "User Workspace", @@ -27,6 +28,7 @@ const Home: React.FC = async () => { fetchLeaves(username); fetchLeaveTypes(); fetchProjectWithTasks(); + fetchHolidays(); return ( diff --git a/src/app/utils/holidayUtils.ts b/src/app/utils/holidayUtils.ts new file mode 100644 index 0000000..2171f41 --- /dev/null +++ b/src/app/utils/holidayUtils.ts @@ -0,0 +1,49 @@ +import Holidays from "date-holidays"; + +const hd = new Holidays("HK"); + +export const getPublicHolidaysForNYears = (years: number = 1) => { + return Array(years) + .fill(undefined) + .flatMap((_, index) => { + const currentYear = new Date().getFullYear(); + const holidays = hd.getHolidays(currentYear + index); + return holidays.map((ele) => { + const tempDay = new Date(ele.date); + const tempYear = tempDay.getFullYear(); + const tempMonth = + tempDay.getMonth() + 1 < 10 + ? `0${tempDay.getMonth() + 1}` + : tempDay.getMonth() + 1; + const tempDate = + tempDay.getDate() < 10 ? `0${tempDay.getDate()}` : tempDay.getDate(); + let tempName = ""; + switch (ele.name) { + case "复活节": + tempName = "復活節"; + break; + case "劳动节": + tempName = "勞動節"; + break; + case "端午节": + tempName = "端午節"; + break; + case "重阳节": + tempName = "重陽節"; + break; + case "圣诞节后的第一个工作日": + tempName = "聖誕節後的第一个工作日"; + break; + default: + tempName = ele.name; + break; + } + + return { + date: `${tempYear}-${tempMonth}-${tempDate}`, + title: tempName, + extendedProps: { calendar: "holiday" }, + }; + }); + }); +}; diff --git a/src/components/utils/useIsMobile.ts b/src/app/utils/useIsMobile.ts similarity index 100% rename from src/components/utils/useIsMobile.ts rename to src/app/utils/useIsMobile.ts diff --git a/src/components/CompanyHoliday/CompanyHoliday.tsx b/src/components/CompanyHoliday/CompanyHoliday.tsx index cdb5ed3..ec92960 100644 --- a/src/components/CompanyHoliday/CompanyHoliday.tsx +++ b/src/components/CompanyHoliday/CompanyHoliday.tsx @@ -1,20 +1,28 @@ "use client"; -import { HolidaysList, HolidaysResult } from "@/app/api/holidays"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Grid, Stack } from '@mui/material/'; +import { HolidaysList } from "@/app/api/holidays"; +import React, { useCallback, useMemo, useState } from "react"; +import { Button, Stack } from "@mui/material/"; import { useTranslation } from "react-i18next"; -import FullCalendar from '@fullcalendar/react' -import dayGridPlugin from '@fullcalendar/daygrid' // a plugin! -import interactionPlugin from "@fullcalendar/interaction" // needed for dayClick -import listPlugin from '@fullcalendar/list'; -import Holidays from "date-holidays"; +import FullCalendar from "@fullcalendar/react"; +import dayGridPlugin from "@fullcalendar/daygrid"; // a plugin! +import interactionPlugin from "@fullcalendar/interaction"; // needed for dayClick +import listPlugin from "@fullcalendar/list"; import CompanyHolidayDialog from "./CompanyHolidayDialog"; -import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm, useFormContext } from "react-hook-form"; -import { EventBusy } from "@mui/icons-material"; -import { deleteCompanyHoliday, saveCompanyHoliday } from "@/app/api/holidays/actions"; +import { + FormProvider, + SubmitErrorHandler, + SubmitHandler, + useForm, + useFormContext, +} from "react-hook-form"; +import { + deleteCompanyHoliday, + saveCompanyHoliday, +} from "@/app/api/holidays/actions"; import { useRouter } from "next/navigation"; import { deleteDialog, submitDialog } from "../Swal/CustomAlerts"; +import { getPublicHolidaysForNYears } from "@/app/utils/holidayUtils"; interface Props { holidays: HolidaysList[]; @@ -23,118 +31,47 @@ interface Props { const CompanyHoliday: React.FC = ({ holidays }) => { const { t } = useTranslation("holidays"); const router = useRouter(); - const formValues = useFormContext(); + const formValues = useFormContext(); const [serverError, setServerError] = useState(""); - const hd = new Holidays('HK') - console.log(holidays) + const companyHolidays = useMemo( + () => [...getPublicHolidaysForNYears(2), ...holidays], + [holidays], + ); - const [companyHolidays, setCompanyHolidays] = useState([]) - const [dateContent, setDateContent] = useState<{ date: string }>({date: ''}) + const [dateContent, setDateContent] = useState<{ date: string }>({ + date: "", + }); const [open, setOpen] = useState(false); const [isEdit, setIsEdit] = useState(false); const [editable, setEditable] = useState(true); const handleClose = () => { setOpen(false); - setEditable(true) - setIsEdit(false) - formProps.setValue("name", "") - formProps.setValue("id", null) + setEditable(true); + setIsEdit(false); + formProps.setValue("name", ""); + formProps.setValue("id", null); }; - const getPublicHolidaysList = () => { - const currentYear = new Date().getFullYear() - const currentYearHolidays = hd.getHolidays(currentYear) - const nextYearHolidays = hd.getHolidays(currentYear + 1) - const events_cyhd = currentYearHolidays.map(ele => { - const tempDay = new Date(ele.date) - const tempYear = tempDay.getFullYear() - const tempMonth = tempDay.getMonth() + 1 < 10 ? `0${ tempDay.getMonth() + 1}` : tempDay.getMonth() + 1 - const tempDate = tempDay.getDate() < 10 ? `0${tempDay.getDate()}` : tempDay.getDate() - let tempName = "" - switch (ele.name) { - case "复活节": - tempName = "復活節" - break - case "劳动节": - tempName = "勞動節" - break - case "端午节": - tempName = "端午節" - break - case "重阳节": - tempName = "重陽節" - break - case "圣诞节后的第一个工作日": - tempName = "聖誕節後的第一个工作日" - break - default: - tempName = ele.name - break - } - - return {date: `${tempYear}-${tempMonth}-${tempDate}`, title: tempName, extendedProps: {calendar: 'holiday'}} - }) - - const events_nyhd = nextYearHolidays.map(ele => { - const tempDay = new Date(ele.date) - const tempYear = tempDay.getFullYear() - const tempMonth = tempDay.getMonth() + 1 < 10 ? `0${ tempDay.getMonth() + 1}` : tempDay.getMonth() + 1 - const tempDate = tempDay.getDate() < 10 ? `0${tempDay.getDate()}` : tempDay.getDate() - let tempName = "" - switch (ele.name) { - case "复活节": - tempName = "復活節" - break - case "劳动节": - tempName = "勞動節" - break - case "端午节": - tempName = "端午節" - break - case "重阳节": - tempName = "重陽節" - break - case "圣诞节后的第一个工作日": - tempName = "聖誕節後的第一个工作日" - break - default: - tempName = ele.name - break - } - return {date: `${tempYear}-${tempMonth}-${tempDate}`, title: tempName, extendedProps: {calendar: 'holiday'}} - }) - - setCompanyHolidays([...events_cyhd, ...events_nyhd, ...holidays] as HolidaysList[]) - } - - useEffect(()=>{ - getPublicHolidaysList() - },[]) - - useEffect(()=>{ - - },[holidays]) - - const handleDateClick = (event:any) => { + const handleDateClick = (event: any) => { // console.log(event.dateStr) - setDateContent({date: event.dateStr}) + setDateContent({ date: event.dateStr }); setOpen(true); - } - - const handleEventClick = (event:any) => { + }; + + const handleEventClick = (event: any) => { // event.event.id: if id !== "", holiday is created by company - console.log(event.event.id) - if (event.event.id === null || event.event.id === ""){ - setEditable(false) + console.log(event.event.id); + if (event.event.id === null || event.event.id === "") { + setEditable(false); } - formProps.setValue("name", event.event.title) - formProps.setValue("id", event.event.id) - setDateContent({date: event.event.startStr}) + formProps.setValue("name", event.event.title); + formProps.setValue("id", event.event.id); + setDateContent({ date: event.event.startStr }); setOpen(true); setIsEdit(true); - } + }; const onSubmit = useCallback>( async (data) => { @@ -142,11 +79,11 @@ const CompanyHoliday: React.FC = ({ holidays }) => { // console.log(data); setServerError(""); submitDialog(async () => { - await saveCompanyHoliday(data) - window.location.reload() + await saveCompanyHoliday(data); + window.location.reload(); setOpen(false); setIsEdit(false); - }, t) + }, t); } catch (e) { console.log(e); setServerError(t("An error has occurred. Please try again later.")); @@ -155,12 +92,12 @@ const CompanyHoliday: React.FC = ({ holidays }) => { [t, router], ); - const handleDelete = async (event:any) => { + const handleDelete = async (event: any) => { try { setServerError(""); deleteDialog(async () => { - await deleteCompanyHoliday(parseInt(formProps.getValues("id"))) - window.location.reload() + await deleteCompanyHoliday(parseInt(formProps.getValues("id"))); + window.location.reload(); setOpen(false); setIsEdit(false); }, t); @@ -168,58 +105,72 @@ const CompanyHoliday: React.FC = ({ holidays }) => { console.log(e); setServerError(t("An error has occurred. Please try again later.")); } - } - - const onSubmitError = useCallback>( - (errors) => { - console.log(errors) - }, - [], - ); - + }; + + const onSubmitError = useCallback>((errors) => { + console.log(errors); + }, []); const formProps = useForm({ defaultValues: { id: null, - name: "" + name: "", }, }); return ( <> - - - - - {isEdit && } - - - } - editable={editable} - /> - + + + + + {isEdit && ( + + )} + + + } + editable={editable} + /> + ); }; diff --git a/src/components/LeaveModal/LeaveModal.tsx b/src/components/LeaveModal/LeaveModal.tsx index 3889a4e..31392d1 100644 --- a/src/components/LeaveModal/LeaveModal.tsx +++ b/src/components/LeaveModal/LeaveModal.tsx @@ -24,7 +24,7 @@ import LeaveTable from "../LeaveTable"; import { LeaveType } from "@/app/api/timesheets"; import FullscreenModal from "../FullscreenModal"; import MobileLeaveTable from "../LeaveTable/MobileLeaveTable"; -import useIsMobile from "../utils/useIsMobile"; +import useIsMobile from "@/app/utils/useIsMobile"; interface Props { isOpen: boolean; diff --git a/src/components/NavigationContent/NavigationContent.tsx b/src/components/NavigationContent/NavigationContent.tsx index 4b47149..df91f50 100644 --- a/src/components/NavigationContent/NavigationContent.tsx +++ b/src/components/NavigationContent/NavigationContent.tsx @@ -18,42 +18,54 @@ import Settings from "@mui/icons-material/Settings"; import Analytics from "@mui/icons-material/Analytics"; import Payments from "@mui/icons-material/Payments"; import Staff from "@mui/icons-material/PeopleAlt"; -import Company from '@mui/icons-material/Store'; -import Department from '@mui/icons-material/Diversity3'; -import Position from '@mui/icons-material/Paragliding'; -import Salary from '@mui/icons-material/AttachMoney'; -import Team from '@mui/icons-material/Paragliding'; -import Holiday from '@mui/icons-material/CalendarMonth'; +import Company from "@mui/icons-material/Store"; +import Department from "@mui/icons-material/Diversity3"; +import Position from "@mui/icons-material/Paragliding"; +import Salary from "@mui/icons-material/AttachMoney"; +import Team from "@mui/icons-material/Paragliding"; +import Holiday from "@mui/icons-material/CalendarMonth"; import { useTranslation } from "react-i18next"; -import Typography from "@mui/material/Typography"; import { usePathname } from "next/navigation"; import Link from "next/link"; import { NAVIGATION_CONTENT_WIDTH } from "@/config/uiConfig"; import Logo from "../Logo"; -import GroupIcon from '@mui/icons-material/Group'; -import BusinessIcon from '@mui/icons-material/Business'; -import ViewWeekIcon from '@mui/icons-material/ViewWeek'; -import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; -import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; -import { GENERATE_REPORTS, MAINTAIN_MASTERDATA, MAINTAIN_USER, VIEW_MASTERDATA, VIEW_USER } from "@/middleware"; +import GroupIcon from "@mui/icons-material/Group"; +import BusinessIcon from "@mui/icons-material/Business"; +import ViewWeekIcon from "@mui/icons-material/ViewWeek"; +import ManageAccountsIcon from "@mui/icons-material/ManageAccounts"; +import EmojiEventsIcon from "@mui/icons-material/EmojiEvents"; +import { + GENERATE_REPORTS, + MAINTAIN_MASTERDATA, + MAINTAIN_USER, + VIEW_MASTERDATA, + VIEW_USER, +} from "@/middleware"; import { SessionWithAbilities } from "../AppBar/NavigationToggle"; import { authOptions } from "@/config/authConfig"; import { getServerSession } from "next-auth"; +import useIsMobile from "@/app/utils/useIsMobile"; interface NavigationItem { icon: React.ReactNode; label: string; path: string; isHidden?: boolean; + showOnMobile?: boolean; children?: NavigationItem[]; } interface Props { - abilities?: string[] + abilities?: string[]; } const NavigationContent: React.FC = ({ abilities }) => { const navigationItems: NavigationItem[] = [ - { icon: , label: "User Workspace", path: "/home" }, + { + icon: , + label: "User Workspace", + path: "/home", + showOnMobile: true, + }, { icon: , label: "Dashboard", @@ -93,7 +105,7 @@ const NavigationContent: React.FC = ({ abilities }) => { icon: , label: "Project Resource Summary", path: "/dashboard/ProjectResourceSummary", - } + }, ], }, { @@ -116,40 +128,109 @@ const NavigationContent: React.FC = ({ abilities }) => { { icon: , label: "Project Management", path: "/projects" }, { icon: , label: "Task Template", path: "/tasks" }, { icon: , label: "Invoice", path: "/invoice" }, - { icon: , label: "Analysis Report", path: "", isHidden: ![GENERATE_REPORTS].some((ability) => abilities!!.includes(ability)), + { + icon: , + label: "Analysis Report", + path: "", + isHidden: ![GENERATE_REPORTS].some((ability) => + abilities!.includes(ability), + ), children: [ - {icon: , label:"Late Start Report", path: "/analytics/LateStartReport"}, - {icon: , label:"Delay Report", path: "/analytics/DelayReport"}, - {icon: , label:"Resource Overconsumption Report", path: "/analytics/ResourceOverconsumptionReport"}, - {icon: , label:"Cost and Expense Report", path: "/analytics/CostandExpenseReport"}, - {icon: , label:"Completion Report", path: "/analytics/ProjectCompletionReport"}, - {icon: , label:"Completion Report with Outstanding Un-billed Hours Report", path: "/analytics/ProjectCompletionReportWO"}, - {icon: , label:"Project Claims Report", path: "/analytics/ProjectClaimsReport"}, - {icon: , label:"Project P&L Report", path: "/analytics/ProjectPLReport"}, - {icon: , label:"Financial Status Report", path: "/analytics/FinancialStatusReport"}, - {icon: , label:"Project Cash Flow Report", path: "/analytics/ProjectCashFlowReport"}, - {icon: , label:"Staff Monthly Work Hours Analysis Report", path: "/analytics/StaffMonthlyWorkHoursAnalysisReport"}, - ], - }, + { + icon: , + label: "Late Start Report", + path: "/analytics/LateStartReport", + }, + { + icon: , + label: "Delay Report", + path: "/analytics/DelayReport", + }, + { + icon: , + label: "Resource Overconsumption Report", + path: "/analytics/ResourceOverconsumptionReport", + }, + { + icon: , + label: "Cost and Expense Report", + path: "/analytics/CostandExpenseReport", + }, + { + icon: , + label: "Completion Report", + path: "/analytics/ProjectCompletionReport", + }, + { + icon: , + label: "Completion Report with Outstanding Un-billed Hours Report", + path: "/analytics/ProjectCompletionReportWO", + }, + { + icon: , + label: "Project Claims Report", + path: "/analytics/ProjectClaimsReport", + }, + { + icon: , + label: "Project P&L Report", + path: "/analytics/ProjectPLReport", + }, + { + icon: , + label: "Financial Status Report", + path: "/analytics/FinancialStatusReport", + }, + { + icon: , + label: "Project Cash Flow Report", + path: "/analytics/ProjectCashFlowReport", + }, + { + icon: , + label: "Staff Monthly Work Hours Analysis Report", + path: "/analytics/StaffMonthlyWorkHoursAnalysisReport", + }, + ], + }, { - icon: , label: "Setting", path: "", isHidden: ![VIEW_MASTERDATA, MAINTAIN_MASTERDATA].some((ability) => abilities!!.includes(ability)), + icon: , + label: "Setting", + path: "", + isHidden: ![VIEW_MASTERDATA, MAINTAIN_MASTERDATA].some((ability) => + abilities!.includes(ability), + ), children: [ { icon: , label: "Client", path: "/settings/customer" }, - { icon: , label: "Subsidiary", path: "/settings/subsidiary" }, + { + icon: , + label: "Subsidiary", + path: "/settings/subsidiary", + }, { icon: , label: "Staff", path: "/settings/staff" }, { icon: , label: "Company", path: "/settings/company" }, { icon: , label: "Skill", path: "/settings/skill" }, - { icon: , label: "Department", path: "/settings/department" }, + { + icon: , + label: "Department", + path: "/settings/department", + }, { icon: , label: "Position", path: "/settings/position" }, { icon: , label: "Salary", path: "/settings/salary" }, { icon: , label: "Team", path: "/settings/team" }, // { icon: , label: "User", path: "/settings/user" }, - { icon: , label: "User Group", path: "/settings/group" }, + { + icon: , + label: "User Group", + path: "/settings/group", + }, { icon: , label: "Holiday", path: "/settings/holiday" }, ], }, ]; + const isMobile = useIsMobile(); + const { t } = useTranslation("common"); const pathname = usePathname(); const [openItems, setOpenItems] = React.useState([]); @@ -197,7 +278,11 @@ const NavigationContent: React.FC = ({ abilities }) => { - {navigationItems.filter(item => item.isHidden !== true).map((item) => renderNavigationItem(item))} + {navigationItems + .filter( + (item) => !item.isHidden && (isMobile ? item.showOnMobile : true), + ) + .map((item) => renderNavigationItem(item))} {/* {navigationItems.map(({ icon, label, path }, index) => { return ( { @@ -95,7 +95,7 @@ const PastEntryCalendarModal: React.FC = ({ return isMobile ? ( - + {t("Past Entries")} = ({ {content} - + {selectedDate && ( + + )} diff --git a/src/components/TimesheetModal/TimesheetModal.tsx b/src/components/TimesheetModal/TimesheetModal.tsx index 6fa0aec..ef1d8a4 100644 --- a/src/components/TimesheetModal/TimesheetModal.tsx +++ b/src/components/TimesheetModal/TimesheetModal.tsx @@ -24,7 +24,7 @@ import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { AssignedProject, ProjectWithTasks } from "@/app/api/projects"; import FullscreenModal from "../FullscreenModal"; import MobileTimesheetTable from "../TimesheetTable/MobileTimesheetTable"; -import useIsMobile from "../utils/useIsMobile"; +import useIsMobile from "@/app/utils/useIsMobile"; interface Props { isOpen: boolean; diff --git a/src/components/TransferList/TransferListWrapper.tsx b/src/components/TransferList/TransferListWrapper.tsx index 520280b..c78766b 100644 --- a/src/components/TransferList/TransferListWrapper.tsx +++ b/src/components/TransferList/TransferListWrapper.tsx @@ -3,7 +3,7 @@ import React from "react"; import TransferList, { TransferListProps } from "./TransferList"; import MultiSelectList from "./MultiSelectList"; -import useIsMobile from "../utils/useIsMobile"; +import useIsMobile from "@/app/utils/useIsMobile"; const TransferListWrapper: React.FC = (props) => { const matches = useIsMobile(); diff --git a/src/components/UserWorkspacePage/UserWorkspacePage.tsx b/src/components/UserWorkspacePage/UserWorkspacePage.tsx index c0e88ef..3948279 100644 --- a/src/components/UserWorkspacePage/UserWorkspacePage.tsx +++ b/src/components/UserWorkspacePage/UserWorkspacePage.tsx @@ -18,6 +18,7 @@ import LeaveModal from "../LeaveModal"; import { LeaveType } from "@/app/api/timesheets"; import { CalendarIcon } from "@mui/x-date-pickers"; import PastEntryCalendarModal from "../PastEntryCalendar/PastEntryCalendarModal"; +import { HolidaysResult } from "@/app/api/holidays"; export interface Props { leaveTypes: LeaveType[]; @@ -26,6 +27,7 @@ export interface Props { username: string; defaultLeaveRecords: RecordLeaveInput; defaultTimesheets: RecordTimesheetInput; + holidays: HolidaysResult[]; } const UserWorkspacePage: React.FC = ({ @@ -35,6 +37,7 @@ const UserWorkspacePage: React.FC = ({ username, defaultLeaveRecords, defaultTimesheets, + holidays }) => { const [isTimeheetModalVisible, setTimeheetModalVisible] = useState(false); const [isLeaveModalVisible, setLeaveModalVisible] = useState(false); diff --git a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx index 783498e..d86df7c 100644 --- a/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx +++ b/src/components/UserWorkspacePage/UserWorkspaceWrapper.tsx @@ -8,20 +8,28 @@ import { fetchLeaves, fetchTimesheets, } from "@/app/api/timesheets"; +import { fetchHolidays } from "@/app/api/holidays"; interface Props { username: string; } const UserWorkspaceWrapper: React.FC = async ({ username }) => { - const [assignedProjects, allProjects, timesheets, leaves, leaveTypes] = - await Promise.all([ - fetchAssignedProjects(username), - fetchProjectWithTasks(), - fetchTimesheets(username), - fetchLeaves(username), - fetchLeaveTypes(), - ]); + const [ + assignedProjects, + allProjects, + timesheets, + leaves, + leaveTypes, + holidays, + ] = await Promise.all([ + fetchAssignedProjects(username), + fetchProjectWithTasks(), + fetchTimesheets(username), + fetchLeaves(username), + fetchLeaveTypes(), + fetchHolidays(), + ]); return ( = async ({ username }) => { defaultTimesheets={timesheets} defaultLeaveRecords={leaves} leaveTypes={leaveTypes} + holidays={holidays} /> ); };