@@ -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"; | "use server"; | ||||
import { serverFetchJson } from "@/app/utils/fetchUtil"; | |||||
import { serverFetchJson, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||||
import { BASE_API_URL } from "@/config/api"; | import { BASE_API_URL } from "@/config/api"; | ||||
import { Dayjs } from "dayjs"; | import { Dayjs } from "dayjs"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
@@ -15,6 +15,7 @@ export interface combo { | |||||
} | } | ||||
export interface CreateCompanyInputs { | export interface CreateCompanyInputs { | ||||
id?: number; | |||||
companyCode: string; | companyCode: string; | ||||
companyName: string; | companyName: string; | ||||
brNo: string; | brNo: string; | ||||
@@ -30,6 +31,23 @@ export interface CreateCompanyInputs { | |||||
email: string; | 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) => { | export const saveCompany = async (data: CreateCompanyInputs) => { | ||||
return serverFetchJson(`${BASE_API_URL}/companys/new`, { | return serverFetchJson(`${BASE_API_URL}/companys/new`, { | ||||
method: "POST", | method: "POST", | ||||
@@ -43,3 +61,15 @@ export const fetchCompanyCombo = cache(async () => { | |||||
next: { tags: ["company"] }, | 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 { BASE_API_URL } from "@/config/api"; | ||||
import { cache } from "react"; | import { cache } from "react"; | ||||
import "server-only"; | import "server-only"; | ||||
import { CreateCompanyInputs, EditCompanyInputs } from "./actions"; | |||||
export interface CompanyResult { | export interface CompanyResult { | ||||
id: number; | id: number; | ||||
@@ -21,4 +22,13 @@ export const fetchCompanys = cache(async () => { | |||||
return serverFetchJson<CompanyResult[]>(`${BASE_API_URL}/companys`, { | return serverFetchJson<CompanyResult[]>(`${BASE_API_URL}/companys`, { | ||||
next: { tags: ["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_DATE_FORMAT = "YYYY/MM/DD"; | ||||
export const OUTPUT_TIME_FORMAT = "HH:mm:ss"; | |||||
export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FORMAT) => { | export const convertDateToString = (date: Date, format: string = OUTPUT_DATE_FORMAT) => { | ||||
return dayjs(date).format(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", { | const shortDateFormatter_en = new Intl.DateTimeFormat("en-HK", { | ||||
weekday: "short", | weekday: "short", | ||||
year: "numeric", | year: "numeric", | ||||
@@ -6,7 +6,10 @@ import SearchBox, { Criterion } from "../SearchBox"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import SearchResults, { Column } from "../SearchResults"; | import SearchResults, { Column } from "../SearchResults"; | ||||
import EditNote from "@mui/icons-material/EditNote"; | 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 { | interface Props { | ||||
companys: CompanyResult[]; | companys: CompanyResult[]; | ||||
@@ -18,6 +21,8 @@ type SearchParamNames = keyof SearchQuery; | |||||
const CompanySearch: React.FC<Props> = ({ companys }) => { | const CompanySearch: React.FC<Props> = ({ companys }) => { | ||||
const { t } = useTranslation("companys"); | const { t } = useTranslation("companys"); | ||||
const router = useRouter() | |||||
const [filteredCompanys, setFilteredCompanys] = useState(companys); | const [filteredCompanys, setFilteredCompanys] = useState(companys); | ||||
const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | const searchCriteria: Criterion<SearchParamNames>[] = useMemo( | ||||
@@ -34,10 +39,22 @@ const CompanySearch: React.FC<Props> = ({ companys }) => { | |||||
setFilteredCompanys(companys); | setFilteredCompanys(companys); | ||||
}, [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>[]>( | const columns = useMemo<Column<CompanyResult>[]>( | ||||
() => [ | () => [ | ||||
{ | { | ||||
@@ -51,7 +68,14 @@ const CompanySearch: React.FC<Props> = ({ companys }) => { | |||||
{ name: "brNo", label: t("brNo") }, | { name: "brNo", label: t("brNo") }, | ||||
{ name: "contactName", label: t("Contact Name") }, | { name: "contactName", label: t("Contact Name") }, | ||||
{ name: "phone", label: t("Contact No.") }, | { 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], | [t, onProjectClick], | ||||
); | ); | ||||
@@ -19,18 +19,39 @@ import { Controller, UseFormRegister, useFormContext } from "react-hook-form"; | |||||
import { CreateCompanyInputs } from "@/app/api/companys/actions"; | import { CreateCompanyInputs } from "@/app/api/companys/actions"; | ||||
import { TimePicker } from "@mui/x-date-pickers"; | import { TimePicker } from "@mui/x-date-pickers"; | ||||
import dayjs from 'dayjs'; | 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 { t } = useTranslation(); | ||||
const { | const { | ||||
register, | register, | ||||
formState: { errors }, | formState: { errors }, | ||||
control, | |||||
setValue, | setValue, | ||||
getValues, | getValues, | ||||
} = useFormContext<CreateCompanyInputs>(); | } = 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 ( | return ( | ||||
<Card> | <Card> | ||||
@@ -101,84 +122,80 @@ const CompanyDetails: React.FC = ({ | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={3}> | <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> | ||||
<Grid item xs={3}> | <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> | ||||
<Grid item xs={3}> | <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> | ||||
<Grid item xs={3}> | <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> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
@@ -4,9 +4,9 @@ import Check from "@mui/icons-material/Check"; | |||||
import Close from "@mui/icons-material/Close"; | import Close from "@mui/icons-material/Close"; | ||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
import Stack from "@mui/material/Stack"; | 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 { useRouter } from "next/navigation"; | ||||
import React, { useCallback, useState } from "react"; | |||||
import React, { useCallback, useDebugValue, useEffect, useState } from "react"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { | import { | ||||
FieldErrors, | FieldErrors, | ||||
@@ -19,9 +19,16 @@ import CompanyDetails from "./CompanyDetails"; | |||||
import { LocalizationProvider } from '@mui/x-date-pickers'; | import { LocalizationProvider } from '@mui/x-date-pickers'; | ||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' | ||||
import dayjs from "dayjs"; | 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 [serverError, setServerError] = useState(""); | ||||
const { t } = useTranslation(); | const { t } = useTranslation(); | ||||
@@ -55,23 +62,24 @@ const CreateCompany: React.FC = ({ | |||||
const formProps = useForm<CreateCompanyInputs>({ | const formProps = useForm<CreateCompanyInputs>({ | ||||
defaultValues: { | 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: "", | otHourTo: "", | ||||
otHourFrom: "", | 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( | return( | ||||
<FormProvider {...formProps}> | <FormProvider {...formProps}> | ||||
@@ -82,7 +90,16 @@ const CreateCompany: React.FC = ({ | |||||
> | > | ||||
{ | { | ||||
<LocalizationProvider dateAdapter={AdapterDayjs}> | <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> | </LocalizationProvider> | ||||
} | } | ||||
@@ -1,8 +1,24 @@ | |||||
import { fetchCompanyDetails } from "@/app/api/companys"; | |||||
import CreateCompany from "./CreateCompany"; | 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 ( | return ( | ||||
<CreateCompany | |||||
<CreateCompany isEdit company={companyDetails} | |||||
/> | /> | ||||
) | ) | ||||
} | } | ||||
@@ -208,7 +208,7 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice }) => { | |||||
{ name: "paymentMilestone", label: t("Payment Milestone") }, | { name: "paymentMilestone", label: t("Payment Milestone") }, | ||||
{ name: "invoiceDate", label: t("Invocie Date") }, | { name: "invoiceDate", label: t("Invocie Date") }, | ||||
{ name: "dueDate", label: t("Due Date") }, | { name: "dueDate", label: t("Due Date") }, | ||||
{ name: "issuedAmount", label: t("Amount (HKD") }, | |||||
{ name: "issuedAmount", label: t("Amount (HKD)") }, | |||||
], | ], | ||||
[t], | [t], | ||||
); | ); | ||||
@@ -316,7 +316,7 @@ const InvoiceSearch: React.FC<Props> = ({ issuedInvoice, receivedInvoice }) => { | |||||
} | } | ||||
<Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | <Tabs value={tabIndex} onChange={handleTabChange} variant="scrollable"> | ||||
<Tab label={t("Issued Invoices")}/> | <Tab label={t("Issued Invoices")}/> | ||||
<Tab label={t("Recieved Invoices")}/> | |||||
<Tab label={t("Received Invoices")}/> | |||||
</Tabs> | </Tabs> | ||||
{ | { | ||||
tabIndex == 0 && | tabIndex == 0 && | ||||