@@ -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 && | |||