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 ( | ||||