diff --git a/src/app/(main)/invoice/new/page.tsx b/src/app/(main)/invoice/new/page.tsx index 6ac8255..10e741f 100644 --- a/src/app/(main)/invoice/new/page.tsx +++ b/src/app/(main)/invoice/new/page.tsx @@ -1,10 +1,11 @@ import { Metadata } from "next"; -import { getServerI18n } from "@/i18n"; +import { I18nProvider, getServerI18n } from "@/i18n"; import Add from "@mui/icons-material/Add"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Link from "next/link"; +import CreateInvoice from "@/components/CreateInvoice"; export const metadata: Metadata = { title: "Create Invoice", @@ -15,9 +16,10 @@ const Invoice: React.FC = async () => { return ( <> - - {t("Create Invoice")} - + {t("Create Invoice")} + + + ) }; diff --git a/src/app/(main)/invoice/page.tsx b/src/app/(main)/invoice/page.tsx index d9d18bf..237ca9c 100644 --- a/src/app/(main)/invoice/page.tsx +++ b/src/app/(main)/invoice/page.tsx @@ -26,14 +26,14 @@ const Invoice: React.FC = async () => { {t("Invoice")} - + */} }> diff --git a/src/app/(main)/settings/position/edit/page.tsx b/src/app/(main)/settings/position/edit/page.tsx new file mode 100644 index 0000000..0eacb0d --- /dev/null +++ b/src/app/(main)/settings/position/edit/page.tsx @@ -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 ( + <> + {t("Edit Position")} + + + + + ); +}; + +export default Positions; \ No newline at end of file diff --git a/src/app/api/invoices/actions.ts b/src/app/api/invoices/actions.ts index c6bdfd2..65778a0 100644 --- a/src/app/api/invoices/actions.ts +++ b/src/app/api/invoices/actions.ts @@ -4,32 +4,42 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; 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(`${BASE_API_URL}/invoices/getProjectDetailById?id=${id}`, { + next: { tags: ["projectDetailById"] }, }); - }; +}) - -export const fetchDepartmentCombo = cache(async () => { - return serverFetchJson(`${BASE_API_URL}/departments/combo`, { - next: { tags: ["department"] }, - }); -}); \ No newline at end of file +export const fetchInvoiceInfoById = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/invoices/getInvoiceInfoById?id=${id}`, { + next: { tags: ["invoiceInfoById"] }, + }); +}) \ No newline at end of file diff --git a/src/app/api/invoices/index.ts b/src/app/api/invoices/index.ts index 7c6fb55..9ab55fe 100644 --- a/src/app/api/invoices/index.ts +++ b/src/app/api/invoices/index.ts @@ -15,6 +15,15 @@ export interface InvoiceResult { reminder: String; } +export interface InvoiceInformatio{ + id: number; + address: string; + attention: string; + invoiceDate: string; + dueDate: string; + projectRefNo: string; +} + export const preloadInvoices = () => { fetchInvoices(); }; diff --git a/src/app/api/positions/actions.ts b/src/app/api/positions/actions.ts index e5abc16..2f69990 100644 --- a/src/app/api/positions/actions.ts +++ b/src/app/api/positions/actions.ts @@ -3,6 +3,7 @@ import { serverFetchJson } from "@/app/utils/fetchUtil"; import { BASE_API_URL } from "@/config/api"; import { cache } from "react"; +import { PositionResult } from "."; export interface comboProp { id: any; @@ -19,6 +20,13 @@ export interface CreatePositionInputs { description: string; } +export interface EditPositionInputs { + id: number; + positionCode: string; + positionName: string; + description: string; +} + export const savePosition = async (data: CreatePositionInputs) => { return serverFetchJson(`${BASE_API_URL}/positions/new`, { 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 () => { return serverFetchJson(`${BASE_API_URL}/positions/combo`, { next: { tags: ["positions"] }, }); +}); + +export const fetchPositionDetails = cache(async (id: number) => { + return serverFetchJson(`${BASE_API_URL}/positions/${id}`, { + next: { tags: ["positionsDetails"] }, + }); }); \ No newline at end of file diff --git a/src/components/CreateInvoice/CreateInvoice.tsx b/src/components/CreateInvoice/CreateInvoice.tsx new file mode 100644 index 0000000..09c4d2b --- /dev/null +++ b/src/components/CreateInvoice/CreateInvoice.tsx @@ -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() + const [invoiceDetail, setInvoiceDetail] = useState() + 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>( + 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>( + (errors) => { + console.log(errors) + }, + [], + ); + + const formProps = useForm({ + defaultValues: { + }, + }); + + const errors = formProps.formState.errors; + + return( + + + { + projectDetail && + } + { + invoiceDetail && + } + + + + + + + + ) +} + +export default CreateInvoice; \ No newline at end of file diff --git a/src/components/CreateInvoice/CreateInvoiceWrapper.tsx b/src/components/CreateInvoice/CreateInvoiceWrapper.tsx new file mode 100644 index 0000000..e1f97a0 --- /dev/null +++ b/src/components/CreateInvoice/CreateInvoiceWrapper.tsx @@ -0,0 +1,11 @@ +import CreateInvoice from "./CreateInvoice"; + +const CreateInvoiceWrapper: React.FC = async () => { + + return ( + + ) +} + +export default CreateInvoiceWrapper; \ No newline at end of file diff --git a/src/components/CreateInvoice/InvoiceDetails.tsx b/src/components/CreateInvoice/InvoiceDetails.tsx new file mode 100644 index 0000000..626ca89 --- /dev/null +++ b/src/components/CreateInvoice/InvoiceDetails.tsx @@ -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 = ({ + invoiceinfo, +}) => { + const { t } = useTranslation(); + const { + register, + formState: { errors }, + control, + setValue, + getValues, + } = useFormContext(); + + console.log(invoiceinfo) + + return ( + + + + + {t("Invoice Information")} + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* + + */} + + + ); +}; + +export default InvoiceDetails; \ No newline at end of file diff --git a/src/components/CreateInvoice/ProjectDetails.tsx b/src/components/CreateInvoice/ProjectDetails.tsx new file mode 100644 index 0000000..f803f72 --- /dev/null +++ b/src/components/CreateInvoice/ProjectDetails.tsx @@ -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 = ({ + projectDetails, +}) => { + const { t } = useTranslation(); + const { + register, + formState: { errors }, + control, + setValue, + getValues, + } = useFormContext(); + + return ( + + + + + {t("Project Details")} + + + + + + + + + + + + + + + + + + + + + + + {/* + + */} + + + ); +}; + +export default ProjectDetails; \ No newline at end of file diff --git a/src/components/CreateInvoice/index.ts b/src/components/CreateInvoice/index.ts new file mode 100644 index 0000000..74f78fc --- /dev/null +++ b/src/components/CreateInvoice/index.ts @@ -0,0 +1 @@ +export { default } from "./CreateInvoiceWrapper" diff --git a/src/components/EditPosition/EditPosition.tsx b/src/components/EditPosition/EditPosition.tsx new file mode 100644 index 0000000..92cf871 --- /dev/null +++ b/src/components/EditPosition/EditPosition.tsx @@ -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() + 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>( + 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>( + (errors) => { + console.log(errors) + }, + [], + ); + + const formProps = useForm({ + // 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 ( + + + { + positionDetails && + } + + + + + + + ); +}; + +export default EditPosition; diff --git a/src/components/EditPosition/EditPositionWrapper.tsx b/src/components/EditPosition/EditPositionWrapper.tsx new file mode 100644 index 0000000..dc531ee --- /dev/null +++ b/src/components/EditPosition/EditPositionWrapper.tsx @@ -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 ( + + ); +}; + +export default EditPositionWrapper; diff --git a/src/components/EditPosition/PositionDetails.tsx b/src/components/EditPosition/PositionDetails.tsx new file mode 100644 index 0000000..086998e --- /dev/null +++ b/src/components/EditPosition/PositionDetails.tsx @@ -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 = ({ + positionDetails, +}) => { + const { t } = useTranslation(); + const { + register, + formState: { errors }, + control, + setValue, + } = useFormContext(); + + return ( + + + + + {t("Position Details")} + + + + + + + + + + + + + + {/* + + */} + + + ); +}; + +export default PositionDetails; \ No newline at end of file diff --git a/src/components/EditPosition/index.ts b/src/components/EditPosition/index.ts new file mode 100644 index 0000000..d6ccc4f --- /dev/null +++ b/src/components/EditPosition/index.ts @@ -0,0 +1 @@ +export { default } from "./EditPositionWrapper" \ No newline at end of file diff --git a/src/components/InvoiceSearch/InvoiceSearch.tsx b/src/components/InvoiceSearch/InvoiceSearch.tsx index 56b6204..52dbdd5 100644 --- a/src/components/InvoiceSearch/InvoiceSearch.tsx +++ b/src/components/InvoiceSearch/InvoiceSearch.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import EditNote from "@mui/icons-material/EditNote"; import { InvoiceResult } from "@/app/api/invoices"; +import { useRouter } from "next/navigation"; interface Props { invoices: InvoiceResult[]; @@ -16,6 +17,7 @@ type SearchParamNames = keyof SearchQuery; const InvoiceSearch: React.FC = ({ invoices }) => { const { t } = useTranslation("invoices"); + const router = useRouter(); const [filteredInvoices, setFilteredInvoices] = useState(invoices); @@ -24,8 +26,18 @@ const InvoiceSearch: React.FC = ({ invoices }) => { { label: t("Project code"), paramName: "projectCode", type: "text" }, { label: t("Project name"), paramName: "projectName", 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("Unbilled hours"), paramName: "unbilledHours", type: "text" }, // { label: t("Reminder to issue invoice"), paramName: "reminder", type: "text" }, @@ -39,7 +51,8 @@ const InvoiceSearch: React.FC = ({ invoices }) => { const onProjectClick = useCallback((project: InvoiceResult) => { console.log(project); - }, []); + router.push(`/invoice/new?id=${project.id}`) + }, [router, t]); const columns = useMemo[]>( () => [ @@ -66,12 +79,12 @@ const InvoiceSearch: React.FC = ({ invoices }) => { { + console.log(query) setFilteredInvoices( invoices.filter( (d) => d.projectCode.toLowerCase().includes(query.projectCode.toLowerCase()) && d.projectName.toLowerCase().includes(query.projectName.toLowerCase()) && - d.stage.toLowerCase().includes(query.stage.toLowerCase()) && {/*(query.client === "All" || p.client === query.client) && (query.category === "All" || p.category === query.category) && (query.team === "All" || p.team === query.team), **/} diff --git a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx index 4312fb9..420736f 100644 --- a/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx +++ b/src/components/InvoiceSearch/InvoiceSearchWrapper.tsx @@ -2,7 +2,6 @@ import React from "react"; import InvoiceSearch from "./InvoiceSearch"; import InvoiceSearchLoading from "./InvoiceSearchLoading"; -// For Later use import { fetchInvoices } from "@/app/api/invoices"; interface SubComponents { @@ -10,7 +9,6 @@ interface SubComponents { } const InvoiceSearchWrapper: React.FC & SubComponents = async () => { - // For Later use const Invoices = await fetchInvoices(); return ; diff --git a/src/components/PositionSearch/PositionSearch.tsx b/src/components/PositionSearch/PositionSearch.tsx index bd3cc4e..d0b3ee4 100644 --- a/src/components/PositionSearch/PositionSearch.tsx +++ b/src/components/PositionSearch/PositionSearch.tsx @@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next"; import SearchResults, { Column } from "../SearchResults"; import EditNote from "@mui/icons-material/EditNote"; import { PositionResult } from "@/app/api/positions"; +import { useRouter } from "next/navigation"; interface Props { positions: PositionResult[]; @@ -16,6 +17,7 @@ type SearchParamNames = keyof SearchQuery; const PositionSearch: React.FC = ({ positions }) => { const { t } = useTranslation("positions"); + const router = useRouter(); const [filteredPositions, setFilteredPositions] = useState(positions); @@ -32,8 +34,10 @@ const PositionSearch: React.FC = ({ positions }) => { setFilteredPositions(positions); }, [positions]); - const onProjectClick = useCallback((project: PositionResult) => { + const onPositionClick = useCallback((project: PositionResult) => { console.log(project); + const id = project.id + router.push(`/settings/position/edit?id=${id}`); }, []); const columns = useMemo[]>( @@ -41,14 +45,14 @@ const PositionSearch: React.FC = ({ positions }) => { { name: "id", label: t("Details"), - onClick: onProjectClick, + onClick: onPositionClick, buttonIcon: , }, { name: "code", label: t("Position Code") }, { name: "name", label: t("Position Name") }, { name: "description", label: t("Position Description") }, ], - [t, onProjectClick], + [t, onPositionClick], ); return (