@@ -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"; |