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