| @@ -15,6 +15,7 @@ | |||
| "@faker-js/faker": "^8.4.1", | |||
| "@fontsource/inter": "^5.0.16", | |||
| "@fontsource/plus-jakarta-sans": "^5.0.18", | |||
| "@fullcalendar/react": "^6.1.11", | |||
| "@mui/icons-material": "^5.15.0", | |||
| "@mui/material": "^5.15.0", | |||
| "@mui/material-nextjs": "^5.15.0", | |||
| @@ -22,6 +23,7 @@ | |||
| "@mui/x-date-pickers": "^6.18.7", | |||
| "@unly/universal-language-detector": "^2.0.3", | |||
| "apexcharts": "^3.45.2", | |||
| "date-holidays": "^3.23.11", | |||
| "dayjs": "^1.11.10", | |||
| "fullcalendar": "^6.1.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"; | |||