| @@ -8,16 +8,36 @@ export interface InvoiceResult { | |||
| id: number; | |||
| projectCode: string; | |||
| projectName: string; | |||
| stage: String; | |||
| comingPaymentMileStone: String; | |||
| paymentMilestoneDate: String; | |||
| stage: string; | |||
| comingPaymentMileStone: string; | |||
| paymentMilestoneDate: string; | |||
| resourceUsage: number; | |||
| unbilledHours: number; | |||
| reminder: String; | |||
| reminder: string; | |||
| } | |||
| export interface CreateInvoiceInputs { | |||
| 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; | |||
| billHours: number; | |||
| } | |||
| @@ -30,16 +50,18 @@ export interface InvoiceInformation{ | |||
| invoiceDate: string; | |||
| dueDate: string; | |||
| projectRefNo: string; | |||
| amount: 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"] }, | |||
| }); | |||
| }) | |||
| 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"] }, | |||
| }); | |||
| }) | |||
| @@ -8,11 +8,11 @@ export interface InvoiceResult { | |||
| projectCode: string; | |||
| projectName: string; | |||
| stage: String; | |||
| comingPaymentMileStone: String; | |||
| paymentMilestoneDate: String; | |||
| comingPaymentMileStone: string; | |||
| paymentMilestoneDate: string; | |||
| resourceUsage: number; | |||
| unbilledHours: number; | |||
| reminder: String; | |||
| reminder: string; | |||
| } | |||
| export interface InvoiceInformatio{ | |||
| @@ -20,6 +20,9 @@ import { InvoiceResult } from "@/app/api/invoices"; | |||
| import { InvoiceInformation, fetchInvoiceInfoById, fetchProjectInvoiceById } from "@/app/api/invoices/actions"; | |||
| import InvoiceDetails from "./InvoiceDetails"; | |||
| import ProjectDetails from "./ProjectDetails"; | |||
| import ProjectTotalFee from "./ProjectTotalFee"; | |||
| import { timestampToDateString } from "@/app/utils/formatUtil"; | |||
| import dayjs from "dayjs"; | |||
| const CreateInvoice: React.FC = ({ | |||
| }) => { | |||
| @@ -31,13 +34,18 @@ const CreateInvoice: React.FC = ({ | |||
| const [invoiceDetail, setInvoiceDetail] = useState<InvoiceInformation>() | |||
| const [serverError, setServerError] = useState(""); | |||
| // const { getValues } = useForm(); | |||
| const fetchProjectDetails = async () =>{ | |||
| const projectId = searchParams.get("id") | |||
| try{ | |||
| if (projectId !== null && parseInt(projectId) > 0) { | |||
| 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){ | |||
| console.log(error) | |||
| @@ -51,7 +59,13 @@ const CreateInvoice: React.FC = ({ | |||
| if (projectId !== null && parseInt(projectId) > 0) { | |||
| const invoiceInfo = await fetchInvoiceInfoById(parseInt(projectId)) | |||
| 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){ | |||
| console.log(error) | |||
| @@ -69,6 +83,7 @@ const CreateInvoice: React.FC = ({ | |||
| }; | |||
| const handlePrintout = () => { | |||
| // const formData = getValues(); | |||
| console.log("Printing in Progress") | |||
| } | |||
| @@ -114,6 +129,9 @@ const CreateInvoice: React.FC = ({ | |||
| { | |||
| invoiceDetail && <InvoiceDetails invoiceinfo={invoiceDetail}/> | |||
| } | |||
| { | |||
| <ProjectTotalFee /> | |||
| } | |||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||
| <Button | |||
| variant="outlined" | |||
| @@ -17,9 +17,12 @@ import RestartAlt from "@mui/icons-material/RestartAlt"; | |||
| import Button from "@mui/material/Button"; | |||
| import { Controller, UseFormRegister, useFormContext } from "react-hook-form"; | |||
| 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 { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
| interface Props { | |||
| invoiceinfo: InvoiceInformation | |||
| @@ -28,16 +31,24 @@ interface Props { | |||
| const InvoiceDetails: React.FC<Props> = ({ | |||
| invoiceinfo, | |||
| }) => { | |||
| const { t } = useTranslation(); | |||
| const { | |||
| t, | |||
| i18n: { language }, | |||
| } = useTranslation(); | |||
| const { | |||
| register, | |||
| formState: { errors }, | |||
| control, | |||
| setValue, | |||
| 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 ( | |||
| <Card> | |||
| @@ -46,6 +57,10 @@ const InvoiceDetails: React.FC<Props> = ({ | |||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||
| {t("Invoice Information")} | |||
| </Typography> | |||
| <LocalizationProvider | |||
| dateAdapter={AdapterDayjs} | |||
| adapterLocale={`${language}-hk`} | |||
| > | |||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| @@ -72,18 +87,28 @@ const InvoiceDetails: React.FC<Props> = ({ | |||
| /> | |||
| </Grid> | |||
| <Grid item xs={6}> | |||
| <TextField | |||
| <FormControl fullWidth> | |||
| <DatePicker | |||
| 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 item xs={6}> | |||
| <TextField | |||
| <FormControl fullWidth> | |||
| <DatePicker | |||
| 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 item xs={6}> | |||
| <TextField | |||
| @@ -103,8 +128,8 @@ const InvoiceDetails: React.FC<Props> = ({ | |||
| error={Boolean(errors.amount)} | |||
| /> | |||
| </Grid> | |||
| </Grid> | |||
| </LocalizationProvider> | |||
| </Box> | |||
| {/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||
| <Button variant="text" startIcon={<RestartAlt />}> | |||
| @@ -14,6 +14,7 @@ import Typography from "@mui/material/Typography"; | |||
| import { useTranslation } from "react-i18next"; | |||
| import { InvoiceResult } from "@/app/api/invoices"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { timestampToDateString } from "@/app/utils/formatUtil"; | |||
| interface Props { | |||
| 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; | |||