| @@ -15,6 +15,7 @@ | |||||
| "@faker-js/faker": "^8.4.1", | "@faker-js/faker": "^8.4.1", | ||||
| "@fontsource/inter": "^5.0.16", | "@fontsource/inter": "^5.0.16", | ||||
| "@fontsource/plus-jakarta-sans": "^5.0.18", | "@fontsource/plus-jakarta-sans": "^5.0.18", | ||||
| "@fullcalendar/react": "^6.1.11", | |||||
| "@mui/icons-material": "^5.15.0", | "@mui/icons-material": "^5.15.0", | ||||
| "@mui/material": "^5.15.0", | "@mui/material": "^5.15.0", | ||||
| "@mui/material-nextjs": "^5.15.0", | "@mui/material-nextjs": "^5.15.0", | ||||
| @@ -22,6 +23,7 @@ | |||||
| "@mui/x-date-pickers": "^6.18.7", | "@mui/x-date-pickers": "^6.18.7", | ||||
| "@unly/universal-language-detector": "^2.0.3", | "@unly/universal-language-detector": "^2.0.3", | ||||
| "apexcharts": "^3.45.2", | "apexcharts": "^3.45.2", | ||||
| "date-holidays": "^3.23.11", | |||||
| "dayjs": "^1.11.10", | "dayjs": "^1.11.10", | ||||
| "fullcalendar": "^6.1.11", | "fullcalendar": "^6.1.11", | ||||
| "i18next": "^23.7.11", | "i18next": "^23.7.11", | ||||
| @@ -0,0 +1,48 @@ | |||||
| import CompanyHoliday from "@/components/CompanyHoliday"; | |||||
| import { Metadata } from "next"; | |||||
| import { getServerI18n } from "@/i18n"; | |||||
| import Add from "@mui/icons-material/Add"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import Link from "next/link"; | |||||
| import { Suspense } from "react"; | |||||
| import { fetchCompanys, preloadCompanys } from "@/app/api/companys"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Holiday", | |||||
| }; | |||||
| const Company: React.FC = async () => { | |||||
| const { t } = await getServerI18n("holiday"); | |||||
| // Preload necessary dependencies | |||||
| return ( | |||||
| <> | |||||
| <Stack | |||||
| direction="row" | |||||
| justifyContent="space-between" | |||||
| flexWrap="wrap" | |||||
| rowGap={2} | |||||
| > | |||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Company Holiday")} | |||||
| </Typography> | |||||
| {/* <Button | |||||
| variant="contained" | |||||
| startIcon={<Add />} | |||||
| LinkComponent={Link} | |||||
| href="/settings/holiday/create" | |||||
| > | |||||
| {t("Create Holiday")} | |||||
| </Button> */} | |||||
| </Stack> | |||||
| <Suspense fallback={<CompanyHoliday.Loading />}> | |||||
| <CompanyHoliday/> | |||||
| </Suspense> | |||||
| </> | |||||
| ) | |||||
| }; | |||||
| export default Company; | |||||
| @@ -0,0 +1,23 @@ | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
| import { BASE_API_URL } from "@/config/api"; | |||||
| import { cache } from "react"; | |||||
| import "server-only"; | |||||
| import EventInput from '@fullcalendar/react'; | |||||
| export interface HolidaysResult extends EventInput { | |||||
| title: string; | |||||
| date: string; | |||||
| extendedProps: { | |||||
| calendar: string; | |||||
| }; | |||||
| } | |||||
| export const preloadCompanys = () => { | |||||
| fetchHolidays(); | |||||
| }; | |||||
| export const fetchHolidays = cache(async () => { | |||||
| return serverFetchJson<HolidaysResult[]>(`${BASE_API_URL}/companys`, { | |||||
| next: { tags: ["companys"] }, | |||||
| }); | |||||
| }); | |||||
| @@ -0,0 +1,166 @@ | |||||
| "use client"; | |||||
| import { 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 { 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 Holidays from "date-holidays"; | |||||
| import CompanyHolidayDialog from "./CompanyHolidayDialog"; | |||||
| import { FormProvider, SubmitErrorHandler, SubmitHandler, useForm } from "react-hook-form"; | |||||
| import { EventBusy } from "@mui/icons-material"; | |||||
| interface Props { | |||||
| holidays: HolidaysResult[]; | |||||
| } | |||||
| const CompanyHoliday: React.FC<Props> = ({ holidays }) => { | |||||
| const { t } = useTranslation("holidays"); | |||||
| const hd = new Holidays('HK') | |||||
| console.log(holidays) | |||||
| const [companyHolidays, setCompanyHolidays] = useState<HolidaysResult[]>([]) | |||||
| const [dateContent, setDateContent] = useState<{ date: string }>({date: ''}) | |||||
| const [open, setOpen] = useState(false); | |||||
| const handleClose = () => { | |||||
| setOpen(false); | |||||
| }; | |||||
| 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] as HolidaysResult[]) | |||||
| } | |||||
| useEffect(()=>{ | |||||
| getPublicHolidaysList() | |||||
| },[]) | |||||
| const handleDateClick = (event:any) => { | |||||
| console.log(event.dateStr) | |||||
| setDateContent({date: event.dateStr}) | |||||
| setOpen(true); | |||||
| } | |||||
| const handleEventClick = (event:any) => { | |||||
| console.log(event) | |||||
| } | |||||
| const onSubmit = useCallback<SubmitHandler<any>>( | |||||
| async (data) => { | |||||
| try { | |||||
| console.log(data); | |||||
| // console.log(JSON.stringify(data)); | |||||
| } catch (e) { | |||||
| console.log(e); | |||||
| } | |||||
| }, | |||||
| [t, ], | |||||
| ); | |||||
| const onSubmitError = useCallback<SubmitErrorHandler<any>>( | |||||
| (errors) => { | |||||
| console.log(errors) | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const formProps = useForm<any>({ | |||||
| defaultValues: { | |||||
| title: "" | |||||
| }, | |||||
| }); | |||||
| return ( | |||||
| <> | |||||
| <FormProvider {...formProps}> | |||||
| <FullCalendar | |||||
| plugins={[ dayGridPlugin, interactionPlugin ]} | |||||
| initialView="dayGridMonth" | |||||
| events={companyHolidays} | |||||
| eventColor='#ff0000' | |||||
| dateClick={handleDateClick} | |||||
| eventClick={handleEventClick} | |||||
| /> | |||||
| <CompanyHolidayDialog | |||||
| open={open} | |||||
| onClose={handleClose} | |||||
| title={("Create Holiday")} | |||||
| content={dateContent} | |||||
| actions={ | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1} component="form" onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)}> | |||||
| <Button onClick={handleClose}>Close</Button> | |||||
| <Button type="submit">Submit</Button> | |||||
| </Stack> | |||||
| } | |||||
| /> | |||||
| </FormProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CompanyHoliday; | |||||
| @@ -0,0 +1,79 @@ | |||||
| import React, { useState } from 'react'; | |||||
| import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Grid, FormControl } from '@mui/material/'; | |||||
| import { useForm, useFormContext } from 'react-hook-form'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| import { DatePicker, LocalizationProvider } from '@mui/x-date-pickers'; | |||||
| import dayjs from 'dayjs'; | |||||
| import { INPUT_DATE_FORMAT } from '@/app/utils/formatUtil'; | |||||
| import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; | |||||
| interface CompanyHolidayDialogProps { | |||||
| open: boolean; | |||||
| onClose: () => void; | |||||
| title: string; | |||||
| actions: React.ReactNode; | |||||
| content: Content | |||||
| } | |||||
| interface Content { | |||||
| date: string | |||||
| } | |||||
| const CompanyHolidayDialog: React.FC<CompanyHolidayDialogProps> = ({ open, onClose, title, actions, content }) => { | |||||
| const { | |||||
| t, | |||||
| i18n: { language }, | |||||
| } = useTranslation(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors }, | |||||
| setValue, | |||||
| getValues, | |||||
| } = useFormContext<any>(); | |||||
| return ( | |||||
| <LocalizationProvider | |||||
| dateAdapter={AdapterDayjs} | |||||
| adapterLocale={`${language}-hk`} | |||||
| > | |||||
| <Dialog open={open} onClose={onClose}> | |||||
| <DialogTitle>{title}</DialogTitle> | |||||
| <DialogContent> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={12}> | |||||
| <TextField | |||||
| label={t("title")} | |||||
| fullWidth | |||||
| {...register("title", { | |||||
| required: "title required!", | |||||
| })} | |||||
| error={Boolean(errors.title)} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={12}> | |||||
| <FormControl fullWidth> | |||||
| <DatePicker | |||||
| label={t("Company Holiday")} | |||||
| value={dayjs(content.date)} | |||||
| onChange={(date) => { | |||||
| if (!date) return; | |||||
| setValue("dueDate", date.format(INPUT_DATE_FORMAT)); | |||||
| }} | |||||
| slotProps={{ | |||||
| textField: { | |||||
| helperText: 'MM/DD/YYYY', | |||||
| }, | |||||
| }} | |||||
| /> | |||||
| </FormControl> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </DialogContent> | |||||
| <DialogActions>{actions}</DialogActions> | |||||
| </Dialog> | |||||
| </LocalizationProvider> | |||||
| ); | |||||
| }; | |||||
| export default CompanyHolidayDialog; | |||||
| @@ -0,0 +1,40 @@ | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import Skeleton from "@mui/material/Skeleton"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import React from "react"; | |||||
| // Can make this nicer | |||||
| export const CompanyHolidayLoading: React.FC = () => { | |||||
| return ( | |||||
| <> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton variant="rounded" height={60} /> | |||||
| <Skeleton | |||||
| variant="rounded" | |||||
| height={50} | |||||
| width={100} | |||||
| sx={{ alignSelf: "flex-end" }} | |||||
| /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| <Card> | |||||
| <CardContent> | |||||
| <Stack spacing={2}> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| <Skeleton variant="rounded" height={40} /> | |||||
| </Stack> | |||||
| </CardContent> | |||||
| </Card> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default CompanyHolidayLoading; | |||||
| @@ -0,0 +1,24 @@ | |||||
| // import { fetchCompanyCategories, fetchCompanys } from "@/app/api/companys"; | |||||
| import React, { useState, } from "react"; | |||||
| import CompanyHoliday from "./CompanyHoliday"; | |||||
| import CompanyHolidayLoading from "./CompanyHolidayLoading"; | |||||
| import { fetchCompanys } from "@/app/api/companys"; | |||||
| import Holidays from "date-holidays"; | |||||
| import { HolidaysResult, fetchHolidays } from "@/app/api/holidays"; | |||||
| interface SubComponents { | |||||
| Loading: typeof CompanyHolidayLoading; | |||||
| } | |||||
| const CompanyHolidayWrapper: React.FC & SubComponents = async () => { | |||||
| // const Companys = await fetchCompanys(); | |||||
| const companyHolidays: HolidaysResult[] = await fetchHolidays() | |||||
| return <CompanyHoliday holidays={companyHolidays} />; | |||||
| }; | |||||
| CompanyHolidayWrapper.Loading = CompanyHolidayLoading; | |||||
| export default CompanyHolidayWrapper; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./CompanyHolidayWrapper"; | |||||