@@ -8,16 +8,36 @@ export interface InvoiceResult { | |||||
id: number; | id: number; | ||||
projectCode: string; | projectCode: string; | ||||
projectName: string; | projectName: string; | ||||
stage: String; | |||||
comingPaymentMileStone: String; | |||||
paymentMilestoneDate: String; | |||||
stage: string; | |||||
comingPaymentMileStone: string; | |||||
paymentMilestoneDate: string; | |||||
resourceUsage: number; | resourceUsage: number; | ||||
unbilledHours: number; | unbilledHours: number; | ||||
reminder: String; | |||||
reminder: string; | |||||
} | } | ||||
export interface CreateInvoiceInputs { | export interface CreateInvoiceInputs { | ||||
id: number; | id: number; | ||||
// Project Details | |||||
projectCode: string; | |||||
projectName: string; | |||||
stage: string; | |||||
comingPaymentMileStone: string; | |||||
paymentMilestoneDate: string; | |||||
resourceUsage: number; | |||||
unbilledHours: number; | |||||
// Invoice Info | |||||
client: string; | |||||
address: string; | |||||
attention: string; | |||||
invoiceDate: string; | |||||
dueDate: string; | |||||
projectRefNo: string; | |||||
// Invoice related Info | |||||
reminder: string; | |||||
amount: number; | amount: number; | ||||
billHours: number; | billHours: number; | ||||
} | } | ||||
@@ -30,16 +50,18 @@ export interface InvoiceInformation{ | |||||
invoiceDate: string; | invoiceDate: string; | ||||
dueDate: string; | dueDate: string; | ||||
projectRefNo: string; | projectRefNo: string; | ||||
amount: number; | |||||
} | } | ||||
export const fetchProjectInvoiceById = cache(async (id: number) => { | export const fetchProjectInvoiceById = cache(async (id: number) => { | ||||
return serverFetchJson<InvoiceResult[]>(`${BASE_API_URL}/invoices/getProjectDetailById?id=${id}`, { | |||||
return serverFetchJson<InvoiceResult[]>(`${BASE_API_URL}/invoices/getProjectDetail/${id}`, { | |||||
next: { tags: ["projectDetailById"] }, | next: { tags: ["projectDetailById"] }, | ||||
}); | }); | ||||
}) | }) | ||||
export const fetchInvoiceInfoById = cache(async (id: number) => { | export const fetchInvoiceInfoById = cache(async (id: number) => { | ||||
return serverFetchJson<InvoiceInformation[]>(`${BASE_API_URL}/invoices/getInvoiceInfoById?id=${id}`, { | |||||
return serverFetchJson<InvoiceInformation[]>(`${BASE_API_URL}/invoices/getInvoiceInfo/${id}`, { | |||||
next: { tags: ["invoiceInfoById"] }, | next: { tags: ["invoiceInfoById"] }, | ||||
}); | }); | ||||
}) | }) |
@@ -8,11 +8,11 @@ export interface InvoiceResult { | |||||
projectCode: string; | projectCode: string; | ||||
projectName: string; | projectName: string; | ||||
stage: String; | stage: String; | ||||
comingPaymentMileStone: String; | |||||
paymentMilestoneDate: String; | |||||
comingPaymentMileStone: string; | |||||
paymentMilestoneDate: string; | |||||
resourceUsage: number; | resourceUsage: number; | ||||
unbilledHours: number; | unbilledHours: number; | ||||
reminder: String; | |||||
reminder: string; | |||||
} | } | ||||
export interface InvoiceInformatio{ | export interface InvoiceInformatio{ | ||||
@@ -20,6 +20,9 @@ import { InvoiceResult } from "@/app/api/invoices"; | |||||
import { InvoiceInformation, fetchInvoiceInfoById, fetchProjectInvoiceById } from "@/app/api/invoices/actions"; | import { InvoiceInformation, fetchInvoiceInfoById, fetchProjectInvoiceById } from "@/app/api/invoices/actions"; | ||||
import InvoiceDetails from "./InvoiceDetails"; | import InvoiceDetails from "./InvoiceDetails"; | ||||
import ProjectDetails from "./ProjectDetails"; | import ProjectDetails from "./ProjectDetails"; | ||||
import ProjectTotalFee from "./ProjectTotalFee"; | |||||
import { timestampToDateString } from "@/app/utils/formatUtil"; | |||||
import dayjs from "dayjs"; | |||||
const CreateInvoice: React.FC = ({ | const CreateInvoice: React.FC = ({ | ||||
}) => { | }) => { | ||||
@@ -31,13 +34,18 @@ const CreateInvoice: React.FC = ({ | |||||
const [invoiceDetail, setInvoiceDetail] = useState<InvoiceInformation>() | const [invoiceDetail, setInvoiceDetail] = useState<InvoiceInformation>() | ||||
const [serverError, setServerError] = useState(""); | const [serverError, setServerError] = useState(""); | ||||
// const { getValues } = useForm(); | |||||
const fetchProjectDetails = async () =>{ | const fetchProjectDetails = async () =>{ | ||||
const projectId = searchParams.get("id") | const projectId = searchParams.get("id") | ||||
try{ | try{ | ||||
if (projectId !== null && parseInt(projectId) > 0) { | if (projectId !== null && parseInt(projectId) > 0) { | ||||
const projectDetail = await fetchProjectInvoiceById(parseInt(projectId)) | const projectDetail = await fetchProjectInvoiceById(parseInt(projectId)) | ||||
console.log(projectDetail) | |||||
setProjectDetail(projectDetail[0]) | |||||
// console.log(projectDetail) | |||||
const updatedPrijectDetail = projectDetail.map(detail => { | |||||
return { ...detail, paymentMilestoneDate: timestampToDateString(detail.paymentMilestoneDate) }; // Update the age of person with id 2 | |||||
}); | |||||
setProjectDetail(updatedPrijectDetail[0]) | |||||
} | } | ||||
} catch (error){ | } catch (error){ | ||||
console.log(error) | console.log(error) | ||||
@@ -51,7 +59,13 @@ const CreateInvoice: React.FC = ({ | |||||
if (projectId !== null && parseInt(projectId) > 0) { | if (projectId !== null && parseInt(projectId) > 0) { | ||||
const invoiceInfo = await fetchInvoiceInfoById(parseInt(projectId)) | const invoiceInfo = await fetchInvoiceInfoById(parseInt(projectId)) | ||||
console.log(invoiceInfo) | console.log(invoiceInfo) | ||||
setInvoiceDetail(invoiceInfo[0]) | |||||
const updatedInvoiceInfo = invoiceInfo.map(info => { | |||||
return { ...info, | |||||
invoiceDate: dayjs.unix(parseFloat(info.invoiceDate)/1000).format('YYYY/MM/DD'), | |||||
dueDate: dayjs.unix(parseFloat(info.dueDate)/1000).format('YYYY/MM/DD') | |||||
}; | |||||
}); | |||||
setInvoiceDetail(updatedInvoiceInfo[0]) | |||||
} | } | ||||
} catch (error){ | } catch (error){ | ||||
console.log(error) | console.log(error) | ||||
@@ -69,6 +83,7 @@ const CreateInvoice: React.FC = ({ | |||||
}; | }; | ||||
const handlePrintout = () => { | const handlePrintout = () => { | ||||
// const formData = getValues(); | |||||
console.log("Printing in Progress") | console.log("Printing in Progress") | ||||
} | } | ||||
@@ -114,6 +129,9 @@ const CreateInvoice: React.FC = ({ | |||||
{ | { | ||||
invoiceDetail && <InvoiceDetails invoiceinfo={invoiceDetail}/> | invoiceDetail && <InvoiceDetails invoiceinfo={invoiceDetail}/> | ||||
} | } | ||||
{ | |||||
<ProjectTotalFee /> | |||||
} | |||||
<Stack direction="row" justifyContent="flex-end" gap={1}> | <Stack direction="row" justifyContent="flex-end" gap={1}> | ||||
<Button | <Button | ||||
variant="outlined" | variant="outlined" | ||||
@@ -17,9 +17,12 @@ import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
import Button from "@mui/material/Button"; | import Button from "@mui/material/Button"; | ||||
import { Controller, UseFormRegister, useFormContext } from "react-hook-form"; | import { Controller, UseFormRegister, useFormContext } from "react-hook-form"; | ||||
import { CreateInvoiceInputs } from "@/app/api/invoices/actions"; | import { CreateInvoiceInputs } from "@/app/api/invoices/actions"; | ||||
import { TimePicker } from "@mui/x-date-pickers"; | |||||
import dayjs from 'dayjs'; | |||||
import { LocalizationProvider, DatePicker } from "@mui/x-date-pickers"; | |||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||||
import dayjs from "dayjs"; | |||||
import { InvoiceInformation } from "@/app/api/invoices/actions"; | import { InvoiceInformation } from "@/app/api/invoices/actions"; | ||||
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||||
interface Props { | interface Props { | ||||
invoiceinfo: InvoiceInformation | invoiceinfo: InvoiceInformation | ||||
@@ -28,16 +31,24 @@ interface Props { | |||||
const InvoiceDetails: React.FC<Props> = ({ | const InvoiceDetails: React.FC<Props> = ({ | ||||
invoiceinfo, | invoiceinfo, | ||||
}) => { | }) => { | ||||
const { t } = useTranslation(); | |||||
const { | |||||
t, | |||||
i18n: { language }, | |||||
} = useTranslation(); | |||||
const { | const { | ||||
register, | register, | ||||
formState: { errors }, | formState: { errors }, | ||||
control, | control, | ||||
setValue, | setValue, | ||||
getValues, | getValues, | ||||
} = useFormContext<CreateInvoiceInputs>(); | |||||
console.log(invoiceinfo) | |||||
} = useFormContext<InvoiceInformation>(); | |||||
useEffect(() => { | |||||
// const invoiceDate = getValues("invoiceDate"); | |||||
// const dueDate = getValues("dueDate"); | |||||
setValue("invoiceDate", dayjs(invoiceinfo.invoiceDate).format(INPUT_DATE_FORMAT)) | |||||
setValue("dueDate", dayjs(invoiceinfo.dueDate).format(INPUT_DATE_FORMAT)) | |||||
}, [invoiceinfo,]); | |||||
return ( | return ( | ||||
<Card> | <Card> | ||||
@@ -46,6 +57,10 @@ const InvoiceDetails: React.FC<Props> = ({ | |||||
<Typography variant="overline" display="block" marginBlockEnd={1}> | <Typography variant="overline" display="block" marginBlockEnd={1}> | ||||
{t("Invoice Information")} | {t("Invoice Information")} | ||||
</Typography> | </Typography> | ||||
<LocalizationProvider | |||||
dateAdapter={AdapterDayjs} | |||||
adapterLocale={`${language}-hk`} | |||||
> | |||||
<Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
@@ -72,18 +87,28 @@ const InvoiceDetails: React.FC<Props> = ({ | |||||
/> | /> | ||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | |||||
<FormControl fullWidth> | |||||
<DatePicker | |||||
label={t("Invoice Date")} | label={t("Invoice Date")} | ||||
fullWidth | |||||
defaultValue={invoiceinfo.invoiceDate} | |||||
value={dayjs(invoiceinfo.invoiceDate)} | |||||
onChange={(date) => { | |||||
if (!date) return; | |||||
setValue("invoiceDate", date.format(INPUT_DATE_FORMAT)); | |||||
}} | |||||
/> | /> | ||||
</FormControl> | |||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | |||||
<FormControl fullWidth> | |||||
<DatePicker | |||||
label={t("Due Date")} | label={t("Due Date")} | ||||
fullWidth | |||||
defaultValue={invoiceinfo.dueDate} | |||||
value={dayjs(invoiceinfo.dueDate)} | |||||
onChange={(date) => { | |||||
if (!date) return; | |||||
setValue("dueDate", date.format(INPUT_DATE_FORMAT)); | |||||
}} | |||||
/> | /> | ||||
</FormControl> | |||||
</Grid> | </Grid> | ||||
<Grid item xs={6}> | <Grid item xs={6}> | ||||
<TextField | <TextField | ||||
@@ -103,8 +128,8 @@ const InvoiceDetails: React.FC<Props> = ({ | |||||
error={Boolean(errors.amount)} | error={Boolean(errors.amount)} | ||||
/> | /> | ||||
</Grid> | </Grid> | ||||
</Grid> | </Grid> | ||||
</LocalizationProvider> | |||||
</Box> | </Box> | ||||
{/* <CardActions sx={{ justifyContent: "flex-end" }}> | {/* <CardActions sx={{ justifyContent: "flex-end" }}> | ||||
<Button variant="text" startIcon={<RestartAlt />}> | <Button variant="text" startIcon={<RestartAlt />}> | ||||
@@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography"; | |||||
import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||
import { InvoiceResult } from "@/app/api/invoices"; | import { InvoiceResult } from "@/app/api/invoices"; | ||||
import { useFormContext } from "react-hook-form"; | import { useFormContext } from "react-hook-form"; | ||||
import { timestampToDateString } from "@/app/utils/formatUtil"; | |||||
interface Props { | interface Props { | ||||
projectDetails: InvoiceResult | projectDetails: InvoiceResult | ||||
@@ -0,0 +1,46 @@ | |||||
import { CreateInvoiceInputs } from "@/app/api/invoices/actions"; | |||||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||||
import { TaskGroup } from "@/app/api/tasks"; | |||||
import { moneyFormatter } from "@/app/utils/formatUtil"; | |||||
import { Divider, Stack, Typography } from "@mui/material"; | |||||
import React from "react"; | |||||
import { useFormContext } from "react-hook-form"; | |||||
import { useTranslation } from "react-i18next"; | |||||
const ProjectTotalFee: React.FC= ({}) => { | |||||
const { t } = useTranslation(); | |||||
const { watch } = useFormContext<CreateInvoiceInputs>(); | |||||
const amount = watch("amount"); | |||||
let projectTotal = 0; | |||||
return ( | |||||
<Stack spacing={1}> | |||||
{/* {taskGroups.map((group, index) => { | |||||
const payments = milestones[group.id]?.payments || []; | |||||
const paymentTotal = payments.reduce((acc, p) => acc + p.amount, 0); | |||||
projectTotal += paymentTotal; | |||||
return ( | |||||
<Stack | |||||
key={`${group.id}-${index}`} | |||||
direction="row" | |||||
justifyContent="space-between" | |||||
> | |||||
<Typography variant="subtitle2">{group.name}</Typography> | |||||
<Typography>{moneyFormatter.format(paymentTotal)}</Typography> | |||||
</Stack> | |||||
); | |||||
})} */} | |||||
<Divider sx={{ paddingBlockStart: 2 }} /> | |||||
<Stack direction="row" justifyContent="space-between"> | |||||
<Typography variant="h6">{t("Project Total Fee")}</Typography> | |||||
<Typography>{moneyFormatter.format(projectTotal += amount)}</Typography> | |||||
</Stack> | |||||
</Stack> | |||||
); | |||||
}; | |||||
export default ProjectTotalFee; |