@@ -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 ( | |||
<> | |||
<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}> | |||
{t("Invoice")} | |||
</Typography> | |||
<Button | |||
{/* <Button | |||
variant="contained" | |||
startIcon={<Add />} | |||
LinkComponent={Link} | |||
href="/invoice/new" | |||
> | |||
{t("Create Invoice")} | |||
</Button> | |||
</Button> */} | |||
</Stack> | |||
<Suspense fallback={<InvoiceSearch.Loading />}> | |||
<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 { 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; | |||
} | |||
export interface InvoiceInformatio{ | |||
id: number; | |||
address: string; | |||
attention: string; | |||
invoiceDate: string; | |||
dueDate: string; | |||
projectRefNo: string; | |||
} | |||
export const preloadInvoices = () => { | |||
fetchInvoices(); | |||
}; | |||
@@ -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<combo>(`${BASE_API_URL}/positions/combo`, { | |||
next: { tags: ["positions"] }, | |||
}); | |||
}); | |||
export const fetchPositionDetails = cache(async (id: number) => { | |||
return serverFetchJson<PositionResult[]>(`${BASE_API_URL}/positions/${id}`, { | |||
next: { tags: ["positionsDetails"] }, | |||
}); | |||
}); |
@@ -39,7 +39,7 @@ export const shortDateFormatter = (locale?: string) => { | |||
} | |||
}; | |||
export function convertLocaleStringToNumber(numberString: String): number { | |||
export function convertLocaleStringToNumber(numberString: string): number { | |||
const numberWithoutCommas = numberString.replace(/,/g, ""); | |||
return parseFloat(numberWithoutCommas); | |||
} |
@@ -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" |
@@ -61,7 +61,9 @@ const hasErrorsInTab = ( | |||
) => { | |||
switch (tabIndex) { | |||
case 0: | |||
return errors.projectName; | |||
return ( | |||
errors.projectName || errors.projectCode || errors.projectDescription | |||
); | |||
default: | |||
false; | |||
} | |||
@@ -101,7 +103,6 @@ const CreateProject: React.FC<Props> = ({ | |||
const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>( | |||
async (data) => { | |||
try { | |||
console.log(data); | |||
setServerError(""); | |||
await saveProject(data); | |||
router.replace("/projects"); | |||
@@ -115,7 +116,11 @@ const CreateProject: React.FC<Props> = ({ | |||
const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | |||
(errors) => { | |||
// Set the tab so that the focus will go there | |||
if (errors.projectName) { | |||
if ( | |||
errors.projectName || | |||
errors.projectDescription || | |||
errors.projectCode | |||
) { | |||
setTabIndex(0); | |||
} | |||
}, | |||
@@ -29,7 +29,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; | |||
import { useFormContext } from "react-hook-form"; | |||
import { useTranslation } from "react-i18next"; | |||
import StyledDataGrid from "../StyledDataGrid"; | |||
import { moneyFormatter } from "@/app/utils/formatUtil"; | |||
import { INPUT_DATE_FORMAT, moneyFormatter } from "@/app/utils/formatUtil"; | |||
import isDate from "lodash/isDate"; | |||
interface Props { | |||
@@ -206,7 +206,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
description: p.description!, | |||
id: p.id!, | |||
amount: p.amount!, | |||
date: dayjs(p.date!).toISOString(), | |||
date: dayjs(p.date!).format(INPUT_DATE_FORMAT), | |||
})), | |||
}, | |||
}); | |||
@@ -245,7 +245,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
...milestones, | |||
[taskGroupId]: { | |||
...milestones[taskGroupId], | |||
startDate: date.toISOString(), | |||
startDate: date.format(INPUT_DATE_FORMAT), | |||
}, | |||
}); | |||
}} | |||
@@ -264,7 +264,7 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
...milestones, | |||
[taskGroupId]: { | |||
...milestones[taskGroupId], | |||
endDate: date.toISOString(), | |||
endDate: date.format(INPUT_DATE_FORMAT), | |||
}, | |||
}); | |||
}} | |||
@@ -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 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<Props> = ({ invoices }) => { | |||
const { t } = useTranslation("invoices"); | |||
const router = useRouter(); | |||
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 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<Props> = ({ invoices }) => { | |||
const onProjectClick = useCallback((project: InvoiceResult) => { | |||
console.log(project); | |||
}, []); | |||
router.push(`/invoice/new?id=${project.id}`) | |||
}, [router, t]); | |||
const columns = useMemo<Column<InvoiceResult>[]>( | |||
() => [ | |||
@@ -66,12 +79,12 @@ const InvoiceSearch: React.FC<Props> = ({ invoices }) => { | |||
<SearchBox | |||
criteria={searchCriteria} | |||
onSearch={(query) => { | |||
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), **/} | |||
@@ -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 <InvoiceSearch invoices={Invoices} />; | |||
@@ -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<Props> = ({ positions }) => { | |||
const { t } = useTranslation("positions"); | |||
const router = useRouter(); | |||
const [filteredPositions, setFilteredPositions] = useState(positions); | |||
@@ -32,8 +34,10 @@ const PositionSearch: React.FC<Props> = ({ 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<Column<PositionResult>[]>( | |||
@@ -41,14 +45,14 @@ const PositionSearch: React.FC<Props> = ({ positions }) => { | |||
{ | |||
name: "id", | |||
label: t("Details"), | |||
onClick: onProjectClick, | |||
onClick: onPositionClick, | |||
buttonIcon: <EditNote />, | |||
}, | |||
{ name: "code", label: t("Position Code") }, | |||
{ name: "name", label: t("Position Name") }, | |||
{ name: "description", label: t("Position Description") }, | |||
], | |||
[t, onProjectClick], | |||
[t, onPositionClick], | |||
); | |||
return ( | |||
@@ -22,6 +22,9 @@ import { AssignedProject } from "@/app/api/projects"; | |||
import uniqBy from "lodash/uniqBy"; | |||
import { TaskGroup } from "@/app/api/tasks"; | |||
import dayjs from "dayjs"; | |||
import isBetween from "dayjs/plugin/isBetween"; | |||
dayjs.extend(isBetween); | |||
const mockProjects: AssignedProject[] = [ | |||
{ | |||