| @@ -0,0 +1,30 @@ | |||
| import CreateCompany from "@/components/CreateCompany"; | |||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||
| import Typography from "@mui/material/Typography"; | |||
| import { Metadata } from "next"; | |||
| export const metadata: Metadata = { | |||
| title: "Create Comapny", | |||
| }; | |||
| interface Props { | |||
| searchParams: { [key: string]: string | undefined }; | |||
| } | |||
| const Companys: React.FC<Props> = async ({searchParams}) => { | |||
| const { t } = await getServerI18n("companys"); | |||
| const companyId = searchParams["id"]; | |||
| return( | |||
| <> | |||
| <Typography variant="h4">{t("Create Company")}</Typography> | |||
| <I18nProvider namespaces={["companys"]}> | |||
| <CreateCompany isEdit={true} companyId={companyId} /> | |||
| </I18nProvider> | |||
| </> | |||
| ) | |||
| } | |||
| export default Companys; | |||
| @@ -1,6 +1,6 @@ | |||
| "use server"; | |||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { Dayjs } from "dayjs"; | |||
| import { cache } from "react"; | |||
| @@ -15,6 +15,7 @@ export interface combo { | |||
| } | |||
| export interface CreateCompanyInputs { | |||
| id?: number; | |||
| companyCode: string; | |||
| companyName: string; | |||
| brNo: string; | |||
| @@ -30,6 +31,23 @@ export interface CreateCompanyInputs { | |||
| email: string; | |||
| } | |||
| export interface EditCompanyInputs { | |||
| id?: number; | |||
| companyCode: string; | |||
| name: string; | |||
| brNo: string; | |||
| contactName: string; | |||
| phone: string; | |||
| otHourTo: number[]; | |||
| otHourFrom: number[]; | |||
| normalHourTo: number[]; | |||
| normalHourFrom: number[]; | |||
| currency: string; | |||
| address: string; | |||
| district: string; | |||
| email: string; | |||
| } | |||
| export const saveCompany = async (data: CreateCompanyInputs) => { | |||
| return serverFetchJson(`${BASE_API_URL}/companys/new`, { | |||
| method: "POST", | |||
| @@ -43,3 +61,15 @@ export const fetchCompanyCombo = cache(async () => { | |||
| next: { tags: ["company"] }, | |||
| }); | |||
| }); | |||
| export const deleteCompany = async (id: number) => { | |||
| const department = await serverFetchWithNoContent( | |||
| `${BASE_API_URL}/companys/${id}`, | |||
| { | |||
| method: "DELETE", | |||
| headers: { "Content-Type": "application/json" }, | |||
| }, | |||
| ); | |||
| return department | |||
| }; | |||
| @@ -2,6 +2,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { cache } from "react"; | |||
| import "server-only"; | |||
| import { CreateCompanyInputs, EditCompanyInputs } from "./actions"; | |||
| export interface CompanyResult { | |||
| id: number; | |||
| @@ -21,4 +22,13 @@ export const fetchCompanys = cache(async () => { | |||
| return serverFetchJson<CompanyResult[]>(`${BASE_API_URL}/companys`, { | |||
| next: { tags: ["companys"] }, | |||
| }); | |||
| }); | |||
| export const fetchCompanyDetails = cache(async (companyId: string) => { | |||
| return serverFetchJson<EditCompanyInputs>( | |||
| `${BASE_API_URL}/companys/companyDetails/${companyId}`, | |||
| { | |||
| next: { tags: [`departmentDetail${companyId}`] }, | |||
| }, | |||
| ); | |||
| }); | |||
| @@ -19,6 +19,8 @@ export const INPUT_DATE_FORMAT = "YYYY-MM-DD"; | |||
| 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) | |||
| } | |||
| @@ -38,6 +40,23 @@ export const convertDateArrayToString = (dateArray: number[], format: string = O | |||
| } | |||
| } | |||
| 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'); | |||
| } | |||
| return timeString | |||
| } | |||
| const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | |||
| weekday: "short", | |||
| year: "numeric", | |||
| @@ -6,7 +6,10 @@ import SearchBox, { Criterion } from "../SearchBox"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| import EditNote from "@mui/icons-material/EditNote"; | |||
| import uniq from "lodash/uniq"; | |||
| import { useRouter } from "next/navigation"; | |||
| import { deleteDialog, successDialog } from "../Swal/CustomAlerts"; | |||
| import { deleteCompany } from "@/app/api/companys/actions"; | |||
| import DeleteIcon from '@mui/icons-material/Delete'; | |||
| interface Props { | |||
| companys: CompanyResult[]; | |||
| @@ -18,6 +21,8 @@ type SearchParamNames = keyof SearchQuery; | |||
| const CompanySearch: React.FC<Props> = ({ companys }) => { | |||
| const { t } = useTranslation("companys"); | |||
| const router = useRouter() | |||
| const [filteredCompanys, setFilteredCompanys] = useState(companys); | |||
| const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | |||
| @@ -34,10 +39,22 @@ const CompanySearch: React.FC<Props> = ({ companys }) => { | |||
| setFilteredCompanys(companys); | |||
| }, [companys]); | |||
| const onProjectClick = useCallback((project: CompanyResult) => { | |||
| console.log(project); | |||
| const onProjectClick = useCallback((company: CompanyResult) => { | |||
| console.log(company); | |||
| router.push(`/settings/company/edit?id=${company.id}`); | |||
| }, []); | |||
| const onDeleteClick = useCallback((company: CompanyResult) => { | |||
| deleteDialog(async() => { | |||
| await deleteCompany(company.id) | |||
| successDialog("Delete Success", t) | |||
| setFilteredCompanys((prev) => prev.filter((obj) => obj.id !== company.id)) | |||
| }, t) | |||
| }, []); | |||
| const columns = useMemo<Column<CompanyResult>[]>( | |||
| () => [ | |||
| { | |||
| @@ -51,7 +68,14 @@ const CompanySearch: React.FC<Props> = ({ companys }) => { | |||
| { name: "brNo", label: t("brNo") }, | |||
| { name: "contactName", label: t("Contact Name") }, | |||
| { name: "phone", label: t("Contact No.") }, | |||
| { name: "email", label: t("Contact Email") } | |||
| { name: "email", label: t("Contact Email") }, | |||
| { | |||
| name: "id", | |||
| label: t("Delete"), | |||
| onClick: onDeleteClick, | |||
| buttonIcon: <DeleteIcon />, | |||
| color: "error" | |||
| }, | |||
| ], | |||
| [t, onProjectClick], | |||
| ); | |||
| @@ -19,18 +19,39 @@ import { Controller, UseFormRegister, useFormContext } from "react-hook-form"; | |||
| import { CreateCompanyInputs } from "@/app/api/companys/actions"; | |||
| import { TimePicker } from "@mui/x-date-pickers"; | |||
| import dayjs from 'dayjs'; | |||
| import { useEffect } from "react"; | |||
| import { convertTimeArrayToString } from "@/app/utils/formatUtil"; | |||
| const CompanyDetails: React.FC = ({ | |||
| interface Props{ | |||
| content : Content; | |||
| } | |||
| interface Content { | |||
| normalHourFrom: number[]; | |||
| normalHourTo: number[]; | |||
| otHourFrom: number[]; | |||
| otHourTo: number[]; | |||
| } | |||
| const CompanyDetails: React.FC<Props> = ({ | |||
| content, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { | |||
| register, | |||
| formState: { errors }, | |||
| control, | |||
| setValue, | |||
| getValues, | |||
| } = useFormContext<CreateCompanyInputs>(); | |||
| console.log(content) | |||
| useEffect(() => { | |||
| setValue("normalHourFrom", convertTimeArrayToString(content.normalHourFrom, "HH:mm:ss", false)); | |||
| setValue("normalHourTo", convertTimeArrayToString(content.normalHourTo, "HH:mm:ss", false)); | |||
| setValue("otHourFrom", convertTimeArrayToString(content.otHourFrom, "HH:mm:ss", false)); | |||
| setValue("otHourTo", convertTimeArrayToString(content.otHourTo, "HH:mm:ss", false)); | |||
| }, [content]) | |||
| return ( | |||
| <Card> | |||
| @@ -101,84 +122,80 @@ const CompanyDetails: React.FC = ({ | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <Controller | |||
| control={control} | |||
| name="normalHourFrom" | |||
| rules={{ required: true }} | |||
| render={({ field }) => { | |||
| return ( | |||
| <TimePicker | |||
| label="Normal Hour From" | |||
| inputRef={field.ref} | |||
| onChange={(time) => { | |||
| const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : ''; | |||
| field.onChange(formattedTime); | |||
| }} | |||
| sx={{ width: '100%' }} | |||
| /> | |||
| ); | |||
| }} | |||
| /> | |||
| <FormControl fullWidth> | |||
| <TimePicker | |||
| label={t("Normal Hour From")} | |||
| value={content.normalHourFrom !== undefined && content.normalHourFrom !== null ? | |||
| dayjs().hour(content.normalHourFrom[0]).minute(content.normalHourFrom[1]) : | |||
| dayjs().hour(9).minute(0)} | |||
| onChange={(time) => { | |||
| if (!time) return; | |||
| setValue("normalHourFrom", time.format("HH:mm:ss")); | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| helperText: 'HH:mm:ss', | |||
| }, | |||
| }} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <Controller | |||
| control={control} | |||
| name="normalHourTo" | |||
| rules={{ required: true }} | |||
| render={({ field }) => { | |||
| return ( | |||
| <TimePicker | |||
| label="Normal Hour To" | |||
| inputRef={field.ref} | |||
| onChange={(time) => { | |||
| const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : ''; | |||
| field.onChange(formattedTime); | |||
| }} | |||
| sx={{ width: '100%' }} | |||
| /> | |||
| ); | |||
| }} | |||
| /> | |||
| <FormControl fullWidth> | |||
| <TimePicker | |||
| label={t("Normal Hour To")} | |||
| value={content.normalHourTo !== undefined && content.normalHourTo !== null ? | |||
| dayjs().hour(content.normalHourTo[0]).minute(content.normalHourTo[1]) : | |||
| dayjs().hour(18).minute(0)} | |||
| onChange={(time) => { | |||
| if (!time) return; | |||
| setValue("normalHourTo", time.format("HH:mm:ss")); | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| helperText: 'HH:mm:ss', | |||
| }, | |||
| }} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <Controller | |||
| control={control} | |||
| name="otHourFrom" | |||
| rules={{ required: true }} | |||
| render={({ field }) => { | |||
| return ( | |||
| <TimePicker | |||
| label="OT Hour From" | |||
| inputRef={field.ref} | |||
| onChange={(time) => { | |||
| const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : ''; | |||
| field.onChange(formattedTime); | |||
| }} | |||
| sx={{ width: '100%' }} | |||
| /> | |||
| ); | |||
| }} | |||
| /> | |||
| <FormControl fullWidth> | |||
| <TimePicker | |||
| label={t("OT Hour From")} | |||
| value={content.otHourFrom !== undefined && content.otHourFrom !== null ? | |||
| dayjs().hour(content.otHourFrom[0]).minute(content.otHourFrom[1]) : | |||
| dayjs().hour(20).minute(0)} | |||
| onChange={(time) => { | |||
| if (!time) return; | |||
| setValue("otHourFrom", time.format("HH:mm:ss")); | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| helperText: 'HH:mm:ss', | |||
| }, | |||
| }} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={3}> | |||
| <Controller | |||
| control={control} | |||
| name="otHourTo" | |||
| rules={{ required: true }} | |||
| render={({ field }) => { | |||
| return ( | |||
| <TimePicker | |||
| label="OT Hour To" | |||
| inputRef={field.ref} | |||
| onChange={(time) => { | |||
| const formattedTime = time ? dayjs(time as string).format('HH:mm:ss') : ''; | |||
| field.onChange(formattedTime); | |||
| }} | |||
| sx={{ width: '100%' }} | |||
| /> | |||
| ); | |||
| }} | |||
| /> | |||
| <FormControl fullWidth> | |||
| <TimePicker | |||
| label={t("OT Hour To")} | |||
| value={content.otHourTo !== undefined && content.otHourTo !== null ? | |||
| dayjs().hour(content.otHourTo[0]).minute(content.otHourTo[1]) : | |||
| dayjs().hour(8).minute(0)} | |||
| onChange={(time) => { | |||
| if (!time) return; | |||
| setValue("otHourTo", time.format("HH:mm:ss")); | |||
| }} | |||
| slotProps={{ | |||
| textField: { | |||
| helperText: 'HH:mm:ss', | |||
| }, | |||
| }} | |||
| /> | |||
| </FormControl> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| @@ -4,9 +4,9 @@ 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 { CreateCompanyInputs, saveCompany } from "@/app/api/companys/actions"; | |||
| import { CreateCompanyInputs, EditCompanyInputs, saveCompany } from "@/app/api/companys/actions"; | |||
| import { useRouter } from "next/navigation"; | |||
| import React, { useCallback, useState } from "react"; | |||
| import React, { useCallback, useDebugValue, useEffect, useState } from "react"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { | |||
| FieldErrors, | |||
| @@ -19,9 +19,16 @@ import CompanyDetails from "./CompanyDetails"; | |||
| import { LocalizationProvider } from '@mui/x-date-pickers'; | |||
| import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' | |||
| import dayjs from "dayjs"; | |||
| import { convertTimeArrayToString } from "@/app/utils/formatUtil"; | |||
| const CreateCompany: React.FC = ({ | |||
| interface Props { | |||
| isEdit: Boolean; | |||
| company?: EditCompanyInputs; | |||
| } | |||
| const CreateCompany: React.FC<Props> = ({ | |||
| isEdit, | |||
| company, | |||
| }) => { | |||
| const [serverError, setServerError] = useState(""); | |||
| const { t } = useTranslation(); | |||
| @@ -55,23 +62,24 @@ const CreateCompany: React.FC = ({ | |||
| const formProps = useForm<CreateCompanyInputs>({ | |||
| defaultValues: { | |||
| companyCode: "", | |||
| companyName: "", | |||
| brNo: "", | |||
| contactName: "", | |||
| phone: "", | |||
| id: company?.id, | |||
| companyCode: company?.companyCode, | |||
| companyName: company?.name, | |||
| brNo: company?.brNo, | |||
| contactName: company?.contactName, | |||
| phone: company?.phone, | |||
| otHourTo: "", | |||
| otHourFrom: "", | |||
| normalHourTo: dayjs().format('HH:mm:ss'), | |||
| normalHourFrom: dayjs().format('HH:mm:ss'), | |||
| currency: "", | |||
| address: "", | |||
| district: "", | |||
| email: "", | |||
| normalHourTo: "", | |||
| normalHourFrom: "", | |||
| currency: company?.currency, | |||
| address: company?.address, | |||
| district: company?.district, | |||
| email: company?.email, | |||
| }, | |||
| }); | |||
| const errors = formProps.formState.errors; | |||
| const errors = formProps.formState.errors; | |||
| return( | |||
| <FormProvider {...formProps}> | |||
| @@ -82,7 +90,16 @@ const CreateCompany: React.FC = ({ | |||
| > | |||
| { | |||
| <LocalizationProvider dateAdapter={AdapterDayjs}> | |||
| <CompanyDetails /> | |||
| <CompanyDetails | |||
| content={ | |||
| { | |||
| normalHourFrom: company?.normalHourFrom as number[], | |||
| normalHourTo: company?.normalHourTo as number[], | |||
| otHourFrom: company?.otHourFrom as number[], | |||
| otHourTo: company?.otHourTo as number[] | |||
| } | |||
| } | |||
| /> | |||
| </LocalizationProvider> | |||
| } | |||
| @@ -1,8 +1,24 @@ | |||
| import { fetchCompanyDetails } from "@/app/api/companys"; | |||
| import CreateCompany from "./CreateCompany"; | |||
| const CreateCompanyWrapper: React.FC = async () => { | |||
| type CreateCompanyProps = {isEdit: false} | |||
| interface EditCompanyProps { | |||
| isEdit: true; | |||
| companyId?: string; | |||
| } | |||
| type Props = CreateCompanyProps | EditCompanyProps; | |||
| const CreateCompanyWrapper: React.FC<Props> = async (props) => { | |||
| console.log(props) | |||
| const companyDetails = props.isEdit | |||
| ? await fetchCompanyDetails(props.companyId!) | |||
| : undefined; | |||
| return ( | |||
| <CreateCompany | |||
| <CreateCompany isEdit company={companyDetails} | |||
| /> | |||
| ) | |||
| } | |||
| @@ -208,7 +208,7 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice }) => { | |||
| { name: "paymentMilestone", label: t("Payment Milestone") }, | |||
| { name: "invoiceDate", label: t("Invocie Date") }, | |||
| { name: "dueDate", label: t("Due Date") }, | |||
| { name: "issuedAmount", label: t("Amount (HKD") }, | |||
| { name: "issuedAmount", label: t("Amount (HKD)") }, | |||
| ], | |||
| [t], | |||
| ); | |||
| @@ -316,7 +316,7 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice }) => { | |||
| } | |||
| <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | |||
| <Tab label={t("Issued Invoices")}/> | |||
| <Tab label={t("Recieved Invoices")}/> | |||
| <Tab label={t("Received Invoices")}/> | |||
| </Tabs> | |||
| { | |||
| tabIndex == 0 && | |||