2. Invoice Search page 3. Create Invoice pagetags/Baseline_30082024_FRONTEND_UAT
| @@ -1,10 +1,11 @@ | |||||
| import { Metadata } from "next"; | import { Metadata } from "next"; | ||||
| import { getServerI18n } from "@/i18n"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import Add from "@mui/icons-material/Add"; | import Add from "@mui/icons-material/Add"; | ||||
| 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 Typography from "@mui/material/Typography"; | import Typography from "@mui/material/Typography"; | ||||
| import Link from "next/link"; | import Link from "next/link"; | ||||
| import CreateInvoice from "@/components/CreateInvoice"; | |||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Create Invoice", | title: "Create Invoice", | ||||
| @@ -15,9 +16,10 @@ const Invoice: React.FC = async () => { | |||||
| return ( | return ( | ||||
| <> | <> | ||||
| <Typography variant="h4" marginInlineEnd={2}> | |||||
| {t("Create Invoice")} | |||||
| </Typography> | |||||
| <Typography variant="h4">{t("Create Invoice")}</Typography> | |||||
| <I18nProvider namespaces={["invoice"]}> | |||||
| <CreateInvoice /> | |||||
| </I18nProvider> | |||||
| </> | </> | ||||
| ) | ) | ||||
| }; | }; | ||||
| @@ -26,14 +26,14 @@ const Invoice: React.FC = async () => { | |||||
| <Typography variant="h4" marginInlineEnd={2}> | <Typography variant="h4" marginInlineEnd={2}> | ||||
| {t("Invoice")} | {t("Invoice")} | ||||
| </Typography> | </Typography> | ||||
| <Button | |||||
| {/* <Button | |||||
| variant="contained" | variant="contained" | ||||
| startIcon={<Add />} | startIcon={<Add />} | ||||
| LinkComponent={Link} | LinkComponent={Link} | ||||
| href="/invoice/new" | href="/invoice/new" | ||||
| > | > | ||||
| {t("Create Invoice")} | {t("Create Invoice")} | ||||
| </Button> | |||||
| </Button> */} | |||||
| </Stack> | </Stack> | ||||
| <Suspense fallback={<InvoiceSearch.Loading />}> | <Suspense fallback={<InvoiceSearch.Loading />}> | ||||
| <InvoiceSearch /> | <InvoiceSearch /> | ||||
| @@ -0,0 +1,25 @@ | |||||
| import EditPosition from "@/components/EditPosition"; | |||||
| import { I18nProvider, getServerI18n } from "@/i18n"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { Metadata } from "next"; | |||||
| export const metadata: Metadata = { | |||||
| title: "Edit Position", | |||||
| }; | |||||
| const Positions: React.FC = async () => { | |||||
| const { t } = await getServerI18n("positions"); | |||||
| // Preload necessary dependencies | |||||
| return ( | |||||
| <> | |||||
| <Typography variant="h4">{t("Edit Position")}</Typography> | |||||
| <I18nProvider namespaces={["positions"]}> | |||||
| <EditPosition /> | |||||
| </I18nProvider> | |||||
| </> | |||||
| ); | |||||
| }; | |||||
| export default Positions; | |||||
| @@ -4,32 +4,42 @@ 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"; | ||||
| export interface comboProp { | |||||
| id: any; | |||||
| label: string; | |||||
| export interface InvoiceResult { | |||||
| id: number; | |||||
| projectCode: string; | |||||
| projectName: string; | |||||
| stage: String; | |||||
| comingPaymentMileStone: String; | |||||
| paymentMilestoneDate: String; | |||||
| resourceUsage: number; | |||||
| unbilledHours: number; | |||||
| reminder: String; | |||||
| } | } | ||||
| export interface combo { | |||||
| records: comboProp[]; | |||||
| export interface CreateInvoiceInputs { | |||||
| id: number; | |||||
| amount: number; | |||||
| billHours: number; | |||||
| } | } | ||||
| export interface CreateDepartmentInputs { | |||||
| departmentCode: string; | |||||
| departmentName: string; | |||||
| description: string; | |||||
| export interface InvoiceInformation{ | |||||
| id: number; | |||||
| client: string; | |||||
| address: string; | |||||
| attention: string; | |||||
| invoiceDate: string; | |||||
| dueDate: string; | |||||
| projectRefNo: string; | |||||
| } | } | ||||
| export const saveDepartment = async (data: CreateDepartmentInputs) => { | |||||
| return serverFetchJson(`${BASE_API_URL}/departments/new`, { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| export const fetchProjectInvoiceById = cache(async (id: number) => { | |||||
| return serverFetchJson<InvoiceResult[]>(`${BASE_API_URL}/invoices/getProjectDetailById?id=${id}`, { | |||||
| next: { tags: ["projectDetailById"] }, | |||||
| }); | }); | ||||
| }; | |||||
| }) | |||||
| export const fetchDepartmentCombo = cache(async () => { | |||||
| return serverFetchJson<combo>(`${BASE_API_URL}/departments/combo`, { | |||||
| next: { tags: ["department"] }, | |||||
| }); | |||||
| }); | |||||
| export const fetchInvoiceInfoById = cache(async (id: number) => { | |||||
| return serverFetchJson<InvoiceInformation[]>(`${BASE_API_URL}/invoices/getInvoiceInfoById?id=${id}`, { | |||||
| next: { tags: ["invoiceInfoById"] }, | |||||
| }); | |||||
| }) | |||||
| @@ -15,6 +15,15 @@ export interface InvoiceResult { | |||||
| reminder: String; | reminder: String; | ||||
| } | } | ||||
| export interface InvoiceInformatio{ | |||||
| id: number; | |||||
| address: string; | |||||
| attention: string; | |||||
| invoiceDate: string; | |||||
| dueDate: string; | |||||
| projectRefNo: string; | |||||
| } | |||||
| export const preloadInvoices = () => { | export const preloadInvoices = () => { | ||||
| fetchInvoices(); | fetchInvoices(); | ||||
| }; | }; | ||||
| @@ -3,6 +3,7 @@ | |||||
| import { serverFetchJson } from "@/app/utils/fetchUtil"; | 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 { PositionResult } from "."; | |||||
| export interface comboProp { | export interface comboProp { | ||||
| id: any; | id: any; | ||||
| @@ -19,6 +20,13 @@ export interface CreatePositionInputs { | |||||
| description: string; | description: string; | ||||
| } | } | ||||
| export interface EditPositionInputs { | |||||
| id: number; | |||||
| positionCode: string; | |||||
| positionName: string; | |||||
| description: string; | |||||
| } | |||||
| export const savePosition = async (data: CreatePositionInputs) => { | export const savePosition = async (data: CreatePositionInputs) => { | ||||
| return serverFetchJson(`${BASE_API_URL}/positions/new`, { | return serverFetchJson(`${BASE_API_URL}/positions/new`, { | ||||
| method: "POST", | method: "POST", | ||||
| @@ -27,9 +35,22 @@ export const savePosition = async (data: CreatePositionInputs) => { | |||||
| }); | }); | ||||
| }; | }; | ||||
| export const editPosition = async (data: EditPositionInputs) => { | |||||
| return serverFetchJson(`${BASE_API_URL}/positions/new`, { | |||||
| method: "POST", | |||||
| body: JSON.stringify(data), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| }; | |||||
| export const fetchPositionCombo = cache(async () => { | export const fetchPositionCombo = cache(async () => { | ||||
| return serverFetchJson<combo>(`${BASE_API_URL}/positions/combo`, { | return serverFetchJson<combo>(`${BASE_API_URL}/positions/combo`, { | ||||
| next: { tags: ["positions"] }, | next: { tags: ["positions"] }, | ||||
| }); | }); | ||||
| }); | |||||
| export const fetchPositionDetails = cache(async (id: number) => { | |||||
| return serverFetchJson<PositionResult[]>(`${BASE_API_URL}/positions/${id}`, { | |||||
| next: { tags: ["positionsDetails"] }, | |||||
| }); | |||||
| }); | }); | ||||
| @@ -0,0 +1,142 @@ | |||||
| "use client"; | |||||
| 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 Print from '@mui/icons-material/Print'; | |||||
| // import { CreateInvoiceInputs, saveInvoice } from "@/app/api/companys/actions"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import React, { useCallback, useState, useLayoutEffect } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { | |||||
| FieldErrors, | |||||
| FormProvider, | |||||
| SubmitErrorHandler, | |||||
| SubmitHandler, | |||||
| useForm, | |||||
| } from "react-hook-form"; | |||||
| import { useSearchParams } from 'next/navigation' | |||||
| import { InvoiceResult } from "@/app/api/invoices"; | |||||
| import { InvoiceInformation, fetchInvoiceInfoById, fetchProjectInvoiceById } from "@/app/api/invoices/actions"; | |||||
| import InvoiceDetails from "./InvoiceDetails"; | |||||
| import ProjectDetails from "./ProjectDetails"; | |||||
| const CreateInvoice: React.FC = ({ | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const searchParams = useSearchParams() | |||||
| const router = useRouter() | |||||
| const [projectDetail, setProjectDetail] = useState<InvoiceResult>() | |||||
| const [invoiceDetail, setInvoiceDetail] = useState<InvoiceInformation>() | |||||
| const [serverError, setServerError] = useState(""); | |||||
| 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]) | |||||
| } | |||||
| } catch (error){ | |||||
| console.log(error) | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| } | |||||
| const fetchInvoiceDetails = async () =>{ | |||||
| const projectId = searchParams.get("id") | |||||
| try{ | |||||
| if (projectId !== null && parseInt(projectId) > 0) { | |||||
| const invoiceInfo = await fetchInvoiceInfoById(parseInt(projectId)) | |||||
| console.log(invoiceInfo) | |||||
| setInvoiceDetail(invoiceInfo[0]) | |||||
| } | |||||
| } catch (error){ | |||||
| console.log(error) | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| } | |||||
| useLayoutEffect(() => { | |||||
| fetchProjectDetails() | |||||
| fetchInvoiceDetails() | |||||
| }, []) | |||||
| const handleCancel = () => { | |||||
| router.back(); | |||||
| }; | |||||
| const handlePrintout = () => { | |||||
| console.log("Printing in Progress") | |||||
| } | |||||
| const onSubmit = useCallback<SubmitHandler<InvoiceResult>>( | |||||
| async (data) => { | |||||
| try { | |||||
| console.log(data); | |||||
| setServerError(""); | |||||
| // console.log(JSON.stringify(data)); | |||||
| // await saveCompany(data) | |||||
| // router.replace("/invoices"); | |||||
| } catch (e) { | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| }, | |||||
| [router, t], | |||||
| ); | |||||
| const onSubmitError = useCallback<SubmitErrorHandler<InvoiceResult>>( | |||||
| (errors) => { | |||||
| console.log(errors) | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const formProps = useForm<InvoiceResult>({ | |||||
| defaultValues: { | |||||
| }, | |||||
| }); | |||||
| const errors = formProps.formState.errors; | |||||
| return( | |||||
| <FormProvider {...formProps}> | |||||
| <Stack | |||||
| spacing={2} | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
| > | |||||
| { | |||||
| projectDetail && <ProjectDetails projectDetails={projectDetail}/> | |||||
| } | |||||
| { | |||||
| invoiceDetail && <InvoiceDetails invoiceinfo={invoiceDetail}/> | |||||
| } | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Close />} | |||||
| onClick={handleCancel} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button variant="contained" startIcon={<Check />} type="submit"> | |||||
| {t("Confirm")} | |||||
| </Button> | |||||
| <Button | |||||
| variant="contained" | |||||
| color="secondary" | |||||
| startIcon={<Print />} | |||||
| onClick={handlePrintout} | |||||
| > | |||||
| {t("Generate Print Out")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| ) | |||||
| } | |||||
| export default CreateInvoice; | |||||
| @@ -0,0 +1,11 @@ | |||||
| import CreateInvoice from "./CreateInvoice"; | |||||
| const CreateInvoiceWrapper: React.FC = async () => { | |||||
| return ( | |||||
| <CreateInvoice | |||||
| /> | |||||
| ) | |||||
| } | |||||
| export default CreateInvoiceWrapper; | |||||
| @@ -0,0 +1,119 @@ | |||||
| "use client"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import FormControl from "@mui/material/FormControl"; | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import InputLabel from "@mui/material/InputLabel"; | |||||
| import MenuItem from "@mui/material/MenuItem"; | |||||
| import Select from "@mui/material/Select"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import CardActions from "@mui/material/CardActions"; | |||||
| 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 { InvoiceInformation } from "@/app/api/invoices/actions"; | |||||
| interface Props { | |||||
| invoiceinfo: InvoiceInformation | |||||
| } | |||||
| const InvoiceDetails: React.FC<Props> = ({ | |||||
| invoiceinfo, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors }, | |||||
| control, | |||||
| setValue, | |||||
| getValues, | |||||
| } = useFormContext<CreateInvoiceInputs>(); | |||||
| console.log(invoiceinfo) | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Invoice Information")} | |||||
| </Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Client")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={invoiceinfo.client} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Address")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={invoiceinfo.address} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Attention")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={invoiceinfo.attention} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Invoice Date")} | |||||
| fullWidth | |||||
| defaultValue={invoiceinfo.invoiceDate} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Due Date")} | |||||
| fullWidth | |||||
| defaultValue={invoiceinfo.dueDate} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Project ref no.")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={invoiceinfo.projectRefNo} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Amount (HKD)")} | |||||
| fullWidth | |||||
| {...register("amount", { | |||||
| required: "Amount required!", | |||||
| })} | |||||
| error={Boolean(errors.amount)} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| {/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
| <Button variant="text" startIcon={<RestartAlt />}> | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </CardActions> */} | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default InvoiceDetails; | |||||
| @@ -0,0 +1,102 @@ | |||||
| "use client"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import FormControl from "@mui/material/FormControl"; | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import InputLabel from "@mui/material/InputLabel"; | |||||
| import MenuItem from "@mui/material/MenuItem"; | |||||
| import Select from "@mui/material/Select"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { InvoiceResult } from "@/app/api/invoices"; | |||||
| import { useFormContext } from "react-hook-form"; | |||||
| interface Props { | |||||
| projectDetails: InvoiceResult | |||||
| } | |||||
| const ProjectDetails: React.FC<Props> = ({ | |||||
| projectDetails, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors }, | |||||
| control, | |||||
| setValue, | |||||
| getValues, | |||||
| } = useFormContext<InvoiceResult>(); | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Project Details")} | |||||
| </Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Poject code")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={projectDetails.projectCode} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Project name")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={projectDetails.projectName} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Stage")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={projectDetails.stage} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Payment milestone date")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={projectDetails.paymentMilestoneDate} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Coming payment milestone")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={projectDetails.comingPaymentMileStone} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Unbilled Hours")} | |||||
| fullWidth | |||||
| disabled | |||||
| defaultValue={projectDetails.unbilledHours} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| {/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
| <Button variant="text" startIcon={<RestartAlt />}> | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </CardActions> */} | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default ProjectDetails; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./CreateInvoiceWrapper" | |||||
| @@ -0,0 +1,144 @@ | |||||
| "use client"; | |||||
| 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 Tab from "@mui/material/Tab"; | |||||
| import Tabs, { TabsProps } from "@mui/material/Tabs"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import React, { useCallback, useState, useLayoutEffect, useEffect } from "react"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import { Task, TaskTemplate } from "@/app/api/tasks"; | |||||
| import { | |||||
| FieldErrors, | |||||
| FormProvider, | |||||
| SubmitErrorHandler, | |||||
| SubmitHandler, | |||||
| useForm, | |||||
| } from "react-hook-form"; | |||||
| import { EditPositionInputs, editPosition, fetchPositionDetails, savePosition } from "@/app/api/positions/actions"; | |||||
| import { Error } from "@mui/icons-material"; | |||||
| import { ProjectCategory } from "@/app/api/projects"; | |||||
| import { Typography } from "@mui/material"; | |||||
| import PositionDetails from "./PositionDetails"; | |||||
| import { useSearchParams } from 'next/navigation' | |||||
| import { PositionResult } from "@/app/api/positions"; | |||||
| const EditPosition: React.FC = ({ | |||||
| // allTasks, | |||||
| // projectCategories, | |||||
| // taskTemplates, | |||||
| // teamLeads, | |||||
| }) => { | |||||
| const [serverError, setServerError] = useState(""); | |||||
| const { t } = useTranslation(); | |||||
| const searchParams = useSearchParams() | |||||
| const router = useRouter(); | |||||
| const [positionDetails, setPositionDetails] = useState<EditPositionInputs>() | |||||
| const positionId = searchParams.get("id") | |||||
| const fetchPositionDetail = async () =>{ | |||||
| console.log(positionId) | |||||
| try{ | |||||
| if (positionId !== null && parseInt(positionId) > 0) { | |||||
| const postionDetails = await fetchPositionDetails(parseInt(positionId)) | |||||
| const updatedArray: EditPositionInputs[] = postionDetails.map((obj) => { | |||||
| return { | |||||
| id: obj.id, | |||||
| positionCode: obj.code, | |||||
| positionName: obj.name, | |||||
| description: obj.description | |||||
| }; | |||||
| }); | |||||
| setPositionDetails(updatedArray[0]) | |||||
| } | |||||
| } catch (error){ | |||||
| console.log(error) | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| } | |||||
| const handleCancel = () => { | |||||
| router.back(); | |||||
| }; | |||||
| const onSubmit = useCallback<SubmitHandler<EditPositionInputs>>( | |||||
| async (data) => { | |||||
| try { | |||||
| if (positionId !== null && parseInt(positionId) > 0) { | |||||
| console.log(data); | |||||
| setServerError(""); | |||||
| // console.log(JSON.stringify(data)); | |||||
| data.id = parseInt(positionId) | |||||
| console.log(data); | |||||
| await editPosition(data) | |||||
| router.replace("/settings/position"); | |||||
| } | |||||
| } catch (e) { | |||||
| setServerError(t("An error has occurred. Please try again later.")); | |||||
| } | |||||
| }, | |||||
| [router, t], | |||||
| ); | |||||
| const onSubmitError = useCallback<SubmitErrorHandler<EditPositionInputs>>( | |||||
| (errors) => { | |||||
| console.log(errors) | |||||
| }, | |||||
| [], | |||||
| ); | |||||
| const formProps = useForm<EditPositionInputs>({ | |||||
| // defaultValues: { | |||||
| // positionCode: positionDetails ? positionDetails.code : "AAA", | |||||
| // positionName: positionDetails ? positionDetails.name : "BBB", | |||||
| // description: positionDetails ? positionDetails.description : "CCC", | |||||
| // }, | |||||
| }); | |||||
| const errors = formProps.formState.errors; | |||||
| useEffect(() => { | |||||
| fetchPositionDetail() | |||||
| }, []) | |||||
| useEffect(() => { | |||||
| if (positionDetails !== null && positionDetails !== undefined) { | |||||
| console.log(positionDetails) | |||||
| formProps.reset(positionDetails) | |||||
| } | |||||
| }, [positionDetails]) | |||||
| return ( | |||||
| <FormProvider {...formProps}> | |||||
| <Stack | |||||
| spacing={2} | |||||
| component="form" | |||||
| onSubmit={formProps.handleSubmit(onSubmit, onSubmitError)} | |||||
| > | |||||
| { | |||||
| positionDetails && <PositionDetails positionDetails={positionDetails}/> | |||||
| } | |||||
| <Stack direction="row" justifyContent="flex-end" gap={1}> | |||||
| <Button | |||||
| variant="outlined" | |||||
| startIcon={<Close />} | |||||
| onClick={handleCancel} | |||||
| > | |||||
| {t("Cancel")} | |||||
| </Button> | |||||
| <Button variant="contained" startIcon={<Check />} type="submit"> | |||||
| {t("Confirm")} | |||||
| </Button> | |||||
| </Stack> | |||||
| </Stack> | |||||
| </FormProvider> | |||||
| ); | |||||
| }; | |||||
| export default EditPosition; | |||||
| @@ -0,0 +1,18 @@ | |||||
| import EditPosition from "./EditPosition"; | |||||
| const EditPositionWrapper: React.FC = async () => { | |||||
| // const [tasks, taskTemplates, PositionCategories, teamLeads] = | |||||
| // await Promise.all([ | |||||
| // fetchAllTasks(), | |||||
| // fetchTaskTemplates(), | |||||
| // fetchPositionCategories(), | |||||
| // fetchTeamLeads(), | |||||
| // ]); | |||||
| return ( | |||||
| <EditPosition | |||||
| /> | |||||
| ); | |||||
| }; | |||||
| export default EditPositionWrapper; | |||||
| @@ -0,0 +1,87 @@ | |||||
| "use client"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import Box from "@mui/material/Box"; | |||||
| import Card from "@mui/material/Card"; | |||||
| import CardContent from "@mui/material/CardContent"; | |||||
| import FormControl from "@mui/material/FormControl"; | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import InputLabel from "@mui/material/InputLabel"; | |||||
| import MenuItem from "@mui/material/MenuItem"; | |||||
| import Select from "@mui/material/Select"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { useTranslation } from "react-i18next"; | |||||
| import CardActions from "@mui/material/CardActions"; | |||||
| import RestartAlt from "@mui/icons-material/RestartAlt"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import { Controller, useFormContext } from "react-hook-form"; | |||||
| import { EditPositionInputs } from "@/app/api/positions/actions"; | |||||
| import { PositionResult } from "@/app/api/positions"; | |||||
| import { useEffect } from "react"; | |||||
| interface Props { | |||||
| positionDetails: EditPositionInputs | |||||
| } | |||||
| const PositionDetails: React.FC<Props> = ({ | |||||
| positionDetails, | |||||
| }) => { | |||||
| const { t } = useTranslation(); | |||||
| const { | |||||
| register, | |||||
| formState: { errors }, | |||||
| control, | |||||
| setValue, | |||||
| } = useFormContext<EditPositionInputs>(); | |||||
| return ( | |||||
| <Card> | |||||
| <CardContent component={Stack} spacing={4}> | |||||
| <Box> | |||||
| <Typography variant="overline" display="block" marginBlockEnd={1}> | |||||
| {t("Position Details")} | |||||
| </Typography> | |||||
| <Grid container spacing={2} columns={{ xs: 6, sm: 12 }}> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Position Code")} | |||||
| fullWidth | |||||
| {...register("positionCode", { | |||||
| required: "Position code required!", | |||||
| })} | |||||
| error={Boolean(errors.positionCode)} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Position Name")} | |||||
| fullWidth | |||||
| {...register("positionName", { | |||||
| required: "Position name required!", | |||||
| })} | |||||
| error={Boolean(errors.positionName)} | |||||
| /> | |||||
| </Grid> | |||||
| <Grid item xs={6}> | |||||
| <TextField | |||||
| label={t("Position Description")} | |||||
| fullWidth | |||||
| {...register("description", { | |||||
| required: "Please enter a description", | |||||
| })} | |||||
| error={Boolean(errors.description)} | |||||
| /> | |||||
| </Grid> | |||||
| </Grid> | |||||
| </Box> | |||||
| {/* <CardActions sx={{ justifyContent: "flex-end" }}> | |||||
| <Button variant="text" startIcon={<RestartAlt />}> | |||||
| {t("Reset")} | |||||
| </Button> | |||||
| </CardActions> */} | |||||
| </CardContent> | |||||
| </Card> | |||||
| ); | |||||
| }; | |||||
| export default PositionDetails; | |||||
| @@ -0,0 +1 @@ | |||||
| export { default } from "./EditPositionWrapper" | |||||
| @@ -6,6 +6,7 @@ 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 { InvoiceResult } from "@/app/api/invoices"; | import { InvoiceResult } from "@/app/api/invoices"; | ||||
| import { useRouter } from "next/navigation"; | |||||
| interface Props { | interface Props { | ||||
| invoices: InvoiceResult[]; | invoices: InvoiceResult[]; | ||||
| @@ -16,6 +17,7 @@ type SearchParamNames = keyof SearchQuery; | |||||
| const InvoiceSearch: React.FC<Props> = ({ invoices }) => { | const InvoiceSearch: React.FC<Props> = ({ invoices }) => { | ||||
| const { t } = useTranslation("invoices"); | const { t } = useTranslation("invoices"); | ||||
| const router = useRouter(); | |||||
| const [filteredInvoices, setFilteredInvoices] = useState(invoices); | const [filteredInvoices, setFilteredInvoices] = useState(invoices); | ||||
| @@ -24,8 +26,18 @@ const InvoiceSearch: React.FC<Props> = ({ invoices }) => { | |||||
| { label: t("Project code"), paramName: "projectCode", type: "text" }, | { label: t("Project code"), paramName: "projectCode", type: "text" }, | ||||
| { label: t("Project name"), paramName: "projectName", type: "text" }, | { label: t("Project name"), paramName: "projectName", type: "text" }, | ||||
| // { label: t("Stage"), paramName: "stage", type: "text" }, | // { label: t("Stage"), paramName: "stage", type: "text" }, | ||||
| { label: t("Coming payment milestone"), paramName: "comingPaymentMileStone", type: "text" }, | |||||
| { label: t("Payment date"), paramName: "paymentMilestoneDate", type: "text" }, | |||||
| { | |||||
| label: t("Coming payment milestone from"), | |||||
| label2: t("Coming payment milestone to"), | |||||
| paramName: "comingPaymentMileStone", | |||||
| type: "dateRange" | |||||
| }, | |||||
| { | |||||
| label: t("Payment date from"), | |||||
| label2: t("Payment date to"), | |||||
| paramName: "paymentMilestoneDate", | |||||
| type: "dateRange" | |||||
| }, | |||||
| // { label: t("Resource utilization %"), paramName: "resourceUsage", type: "text" }, | // { label: t("Resource utilization %"), paramName: "resourceUsage", type: "text" }, | ||||
| // { label: t("Unbilled hours"), paramName: "unbilledHours", type: "text" }, | // { label: t("Unbilled hours"), paramName: "unbilledHours", type: "text" }, | ||||
| // { label: t("Reminder to issue invoice"), paramName: "reminder", type: "text" }, | // { label: t("Reminder to issue invoice"), paramName: "reminder", type: "text" }, | ||||
| @@ -39,7 +51,8 @@ const InvoiceSearch: React.FC<Props> = ({ invoices }) => { | |||||
| const onProjectClick = useCallback((project: InvoiceResult) => { | const onProjectClick = useCallback((project: InvoiceResult) => { | ||||
| console.log(project); | console.log(project); | ||||
| }, []); | |||||
| router.push(`/invoice/new?id=${project.id}`) | |||||
| }, [router, t]); | |||||
| const columns = useMemo<Column<InvoiceResult>[]>( | const columns = useMemo<Column<InvoiceResult>[]>( | ||||
| () => [ | () => [ | ||||
| @@ -66,12 +79,12 @@ const InvoiceSearch: React.FC<Props> = ({ invoices }) => { | |||||
| <SearchBox | <SearchBox | ||||
| criteria={searchCriteria} | criteria={searchCriteria} | ||||
| onSearch={(query) => { | onSearch={(query) => { | ||||
| console.log(query) | |||||
| setFilteredInvoices( | setFilteredInvoices( | ||||
| invoices.filter( | invoices.filter( | ||||
| (d) => | (d) => | ||||
| d.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && | d.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && | ||||
| d.projectName.toLowerCase().includes(query.projectName.toLowerCase()) && | d.projectName.toLowerCase().includes(query.projectName.toLowerCase()) && | ||||
| d.stage.toLowerCase().includes(query.stage.toLowerCase()) && | |||||
| {/*(query.client === "All" || p.client === query.client) && | {/*(query.client === "All" || p.client === query.client) && | ||||
| (query.category === "All" || p.category === query.category) && | (query.category === "All" || p.category === query.category) && | ||||
| (query.team === "All" || p.team === query.team), **/} | (query.team === "All" || p.team === query.team), **/} | ||||
| @@ -2,7 +2,6 @@ | |||||
| import React from "react"; | import React from "react"; | ||||
| import InvoiceSearch from "./InvoiceSearch"; | import InvoiceSearch from "./InvoiceSearch"; | ||||
| import InvoiceSearchLoading from "./InvoiceSearchLoading"; | import InvoiceSearchLoading from "./InvoiceSearchLoading"; | ||||
| // For Later use | |||||
| import { fetchInvoices } from "@/app/api/invoices"; | import { fetchInvoices } from "@/app/api/invoices"; | ||||
| interface SubComponents { | interface SubComponents { | ||||
| @@ -10,7 +9,6 @@ interface SubComponents { | |||||
| } | } | ||||
| const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | const InvoiceSearchWrapper: React.FC & SubComponents = async () => { | ||||
| // For Later use | |||||
| const Invoices = await fetchInvoices(); | const Invoices = await fetchInvoices(); | ||||
| return <InvoiceSearch invoices={Invoices} />; | return <InvoiceSearch invoices={Invoices} />; | ||||
| @@ -6,6 +6,7 @@ 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 { PositionResult } from "@/app/api/positions"; | import { PositionResult } from "@/app/api/positions"; | ||||
| import { useRouter } from "next/navigation"; | |||||
| interface Props { | interface Props { | ||||
| positions: PositionResult[]; | positions: PositionResult[]; | ||||
| @@ -16,6 +17,7 @@ type SearchParamNames = keyof SearchQuery; | |||||
| const PositionSearch: React.FC<Props> = ({ positions }) => { | const PositionSearch: React.FC<Props> = ({ positions }) => { | ||||
| const { t } = useTranslation("positions"); | const { t } = useTranslation("positions"); | ||||
| const router = useRouter(); | |||||
| const [filteredPositions, setFilteredPositions] = useState(positions); | const [filteredPositions, setFilteredPositions] = useState(positions); | ||||
| @@ -32,8 +34,10 @@ const PositionSearch: React.FC<Props> = ({ positions }) => { | |||||
| setFilteredPositions(positions); | setFilteredPositions(positions); | ||||
| }, [positions]); | }, [positions]); | ||||
| const onProjectClick = useCallback((project: PositionResult) => { | |||||
| const onPositionClick = useCallback((project: PositionResult) => { | |||||
| console.log(project); | console.log(project); | ||||
| const id = project.id | |||||
| router.push(`/settings/position/edit?id=${id}`); | |||||
| }, []); | }, []); | ||||
| const columns = useMemo<Column<PositionResult>[]>( | const columns = useMemo<Column<PositionResult>[]>( | ||||
| @@ -41,14 +45,14 @@ const PositionSearch: React.FC<Props> = ({ positions }) => { | |||||
| { | { | ||||
| name: "id", | name: "id", | ||||
| label: t("Details"), | label: t("Details"), | ||||
| onClick: onProjectClick, | |||||
| onClick: onPositionClick, | |||||
| buttonIcon: <EditNote />, | buttonIcon: <EditNote />, | ||||
| }, | }, | ||||
| { name: "code", label: t("Position Code") }, | { name: "code", label: t("Position Code") }, | ||||
| { name: "name", label: t("Position Name") }, | { name: "name", label: t("Position Name") }, | ||||
| { name: "description", label: t("Position Description") }, | { name: "description", label: t("Position Description") }, | ||||
| ], | ], | ||||
| [t, onProjectClick], | |||||
| [t, onPositionClick], | |||||
| ); | ); | ||||
| return ( | return ( | ||||