| @@ -2,7 +2,7 @@ | |||
| import { serverFetchJson, serverFetchString, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; | |||
| import { BASE_API_URL } from "@/config/api"; | |||
| import { revalidateTag } from "next/cache"; | |||
| import { revalidatePath, revalidateTag } from "next/cache"; | |||
| import { cache } from "react"; | |||
| export interface InvoiceResult { | |||
| @@ -145,8 +145,8 @@ export const updateInvoice = async (data: any) => { | |||
| body: JSON.stringify(data), | |||
| headers: { "Content-Type": "application/json" }, | |||
| }); | |||
| revalidateTag("invoices") | |||
| revalidatePath("/(main)/invoice") | |||
| return updateInvoice; | |||
| } | |||
| @@ -32,7 +32,7 @@ export const truncateMoney = (amount: number | undefined) => { | |||
| const fractionDigits = maximumFractionDigits ?? minimumFractionDigits ?? 0; | |||
| const factor = Math.pow(10, fractionDigits); | |||
| const truncatedAmount = Math.floor(amount * factor) / factor; | |||
| const truncatedAmount = Math.round(amount * factor) / factor; | |||
| return truncatedAmount; | |||
| }; | |||
| @@ -59,7 +59,7 @@ import { | |||
| } from "../Swal/CustomAlerts"; | |||
| import dayjs from "dayjs"; | |||
| import { DELETE_PROJECT } from "@/middleware"; | |||
| import { OUTPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import { OUTPUT_DATE_FORMAT, sumMoney } from "@/app/utils/formatUtil"; | |||
| import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUtils"; | |||
| export interface Props { | |||
| @@ -273,7 +273,7 @@ const CreateProject: React.FC<Props> = ({ | |||
| const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>( | |||
| async (data, event) => { | |||
| try { | |||
| console.log(data); | |||
| // console.log(data); | |||
| // detect errors | |||
| let hasErrors = false; | |||
| @@ -374,30 +374,43 @@ const CreateProject: React.FC<Props> = ({ | |||
| data.milestones[parseFloat(key)].startDate = null | |||
| data.milestones[parseFloat(key)].endDate = null | |||
| } | |||
| // if ( | |||
| // !Boolean(startDate) || | |||
| // startDate === "Invalid Date" || | |||
| // !Boolean(endDate) || | |||
| // endDate === "Invalid Date" || | |||
| // new Date(startDate) > new Date(endDate) | |||
| // ) { | |||
| // formProps.setError("milestones", { | |||
| // message: "milestones is not valid", | |||
| // type: "invalid", | |||
| // }); | |||
| // setTabIndex(3); | |||
| // hasErrors = true; | |||
| // } | |||
| /* | |||
| unused code for checking the stage start date and end date | |||
| if ( | |||
| !Boolean(startDate) || | |||
| startDate === "Invalid Date" || | |||
| !Boolean(endDate) || | |||
| endDate === "Invalid Date" || | |||
| new Date(startDate) > new Date(endDate) | |||
| ) { | |||
| formProps.setError("milestones", { | |||
| message: "milestones is not valid", | |||
| type: "invalid", | |||
| }); | |||
| setTabIndex(3); | |||
| hasErrors = true; | |||
| } | |||
| unused code for bulk add milestone payment | |||
| projectTotal += payments.reduce( | |||
| (acc, payment) => acc + payment.amount, | |||
| 0, | |||
| ); | |||
| **/ | |||
| projectTotal += payments.reduce( | |||
| (acc, p) => sumMoney(acc, p.amount), | |||
| 0, | |||
| ); | |||
| }); | |||
| // console.log(projectTotal) | |||
| console.log(milestonesKeys) | |||
| if ( | |||
| projectTotal !== data.expectedProjectFee || | |||
| milestonesKeys.length !== taskGroupKeys.length | |||
| projectTotal !== data.expectedProjectFee | |||
| // || milestonesKeys.length !== taskGroupKeys.length | |||
| ) { | |||
| formProps.setError("milestones", { | |||
| message: "milestones is not valid", | |||
| @@ -484,7 +497,7 @@ const CreateProject: React.FC<Props> = ({ | |||
| setServerError(t("An error has occurred. Please try again later.")); | |||
| } | |||
| }, | |||
| [router, t], | |||
| [router, t, draftId], | |||
| ); | |||
| const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( | |||
| @@ -565,6 +578,7 @@ const CreateProject: React.FC<Props> = ({ | |||
| const saveDraft = useCallback(async () => { | |||
| const currentTimestamp = Date.now() | |||
| console.log(currentTimestamp) | |||
| saveToLocalStorage(draftId || currentTimestamp, formProps.getValues()); | |||
| @@ -83,7 +83,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => { | |||
| var filteredTeamLeads = teamId ? teamLeads.filter( | |||
| (teamLead) => teamLead.teamId === teamId, | |||
| ) : teamLeads | |||
| if (userStaff?.id !== null && userStaff?.id == 1) { | |||
| if (userStaff?.id != null && userStaff?.id == 1) { | |||
| filteredTeamLeads = teamLeads.filter( | |||
| (teamLead) => teamLead.teamId === teamId || teamLead.team == "ST", | |||
| ) | |||
| @@ -23,6 +23,7 @@ import { useFormContext } from "react-hook-form"; | |||
| import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
| import MilestoneSection from "./MilestoneSection"; | |||
| import ProjectTotalFee from "./ProjectTotalFee"; | |||
| import { sumMoney } from "@/app/utils/formatUtil"; | |||
| export interface Props { | |||
| allTasks: Task[]; | |||
| @@ -89,10 +90,17 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => { | |||
| // hasError = true | |||
| // } | |||
| projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0) | |||
| // projectTotal += payments.reduce((acc, payment) => acc + payment.amount, 0) | |||
| projectTotal += payments.reduce( | |||
| (acc, p) => sumMoney(acc, p.amount), | |||
| 0, | |||
| ); | |||
| }) | |||
| console.log(milestonesKeys) | |||
| if (projectTotal !== expectedTotalFee || milestonesKeys.length !== taskGroupsIds.length) { | |||
| // if (projectTotal !== expectedTotalFee || milestonesKeys.length !== taskGroupsIds.length) { | |||
| if (projectTotal !== expectedTotalFee) { | |||
| hasError = true | |||
| } | |||
| // console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) | |||
| @@ -242,18 +242,18 @@ const ProjectClientDetails: React.FC<Props> = ({ | |||
| // (acc, wn) => ({ ...acc, [wn.id]: wn.name }), | |||
| // {}, | |||
| // ); | |||
| const planStart = getValues("projectPlanStart") | |||
| const planEnd = getValues("projectPlanEnd") | |||
| const planStart = watch("projectPlanStart") | |||
| const planEnd = watch("projectPlanEnd") | |||
| useEffect(() => { | |||
| let hasErrors = false | |||
| if( | |||
| !planStart || planStart > planEnd | |||
| !planStart || new Date(planStart) > new Date(planEnd) | |||
| ){ | |||
| hasErrors = true; | |||
| } | |||
| if( | |||
| !planEnd || planStart > planEnd | |||
| !planEnd || new Date(planStart) > new Date(planEnd) | |||
| ){ | |||
| hasErrors = true; | |||
| } | |||
| @@ -26,7 +26,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; | |||
| import { DemoItem } from "@mui/x-date-pickers/internals/demo"; | |||
| import dayjs from "dayjs"; | |||
| import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; | |||
| import TableModal from "./TableModal"; | |||
| // import TableModal from "./TableModal"; | |||
| import { Preview } from "@mui/icons-material"; | |||
| import SalaryEffectiveModel from "../EditStaff/SalaryEffectiveModel"; | |||
| import TeamHistoryModal from "../EditStaff/TeamHistoryModal"; | |||
| @@ -69,7 +69,7 @@ const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects, invoice | |||
| if (response === "OK") { | |||
| onClose() | |||
| successDialog(t("Submit Success"), t).then(() => { | |||
| router.replace("/invoice"); | |||
| // router.replace("/invoice"); | |||
| }) | |||
| } | |||
| }, t) | |||
| @@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next"; | |||
| import SearchResults, { Column } from "../SearchResults"; | |||
| import EditNote from "@mui/icons-material/EditNote"; | |||
| import { moneyFormatter } from "@/app/utils/formatUtil" | |||
| import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, TextField, CardContent, Typography, Divider, Card } from "@mui/material"; | |||
| import { Button, ButtonGroup, Stack, Tab, Tabs, TabsProps, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, TextField, CardContent, Typography, Divider, Card, ThemeProvider } from "@mui/material"; | |||
| import FileUploadIcon from '@mui/icons-material/FileUpload'; | |||
| import AddIcon from '@mui/icons-material/Add'; | |||
| import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoice } from "@/app/api/invoices/actions"; | |||
| @@ -23,6 +23,8 @@ import { ProjectResult } from "@/app/api/projects"; | |||
| import { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware"; | |||
| import InvoiceSearchLoading from "./InvoiceSearchLoading"; | |||
| import { NumberFormatValues, NumericFormat } from "react-number-format"; | |||
| import theme from "@/theme"; | |||
| import { SMALL_ROW_THEME } from "@/theme/colorConst"; | |||
| interface CustomMoneyComponentProps { | |||
| params: GridRenderEditCellParams; | |||
| @@ -614,11 +616,13 @@ const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, ab | |||
| { | |||
| !loading && | |||
| // tabIndex == 0 && | |||
| <ThemeProvider theme={SMALL_ROW_THEME}> | |||
| <SearchResults<invoiceList> | |||
| items={filteredIvoices} | |||
| columns={combinedColumns} | |||
| autoRedirectToFirstPage | |||
| /> | |||
| </ThemeProvider> | |||
| } | |||
| <Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="lg" fullWidth> | |||
| <DialogTitle>{t("Edit Invoice")}</DialogTitle> | |||
| @@ -13,12 +13,14 @@ interface Props { | |||
| leaveTypes: LeaveType[]; | |||
| timesheetRecords: RecordTimesheetInput; | |||
| companyHolidays: HolidaysResult[]; | |||
| isSaturdayWorker: boolean; | |||
| } | |||
| const LeaveTable: React.FC<Props> = ({ | |||
| leaveTypes, | |||
| timesheetRecords, | |||
| companyHolidays, | |||
| isSaturdayWorker | |||
| }) => { | |||
| const { watch } = useFormContext<RecordLeaveInput>(); | |||
| const currentInput = watch(); | |||
| @@ -32,6 +34,7 @@ const LeaveTable: React.FC<Props> = ({ | |||
| timesheetEntries={timesheetRecords} | |||
| EntryTableComponent={LeaveEntryTable} | |||
| entryTableProps={{ leaveTypes }} | |||
| isSaturdayWorker={isSaturdayWorker} | |||
| /> | |||
| ); | |||
| }; | |||
| @@ -110,10 +110,19 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||
| path: "/home", | |||
| showOnMobile: true, | |||
| }, | |||
| // { | |||
| // icon: <SummarizeIcon />, | |||
| // label: "Financial Summary", | |||
| // path: "/dashboard/ProjectFinancialSummary", | |||
| // isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => | |||
| // abilities!.includes(ability), | |||
| // ), | |||
| // showOnMobile: false, | |||
| // }, | |||
| { | |||
| icon: <SummarizeIcon />, | |||
| label: "Financial Summary", | |||
| path: "/dashboard/ProjectFinancialSummary", | |||
| path: "/dashboard/ProjectFinancialSummaryV2", | |||
| isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => | |||
| abilities!.includes(ability), | |||
| ), | |||
| @@ -173,16 +182,6 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => { | |||
| ], | |||
| }, | |||
| // { | |||
| // icon: <SummarizeIcon />, | |||
| // label: "Financial Summary", | |||
| // path: "/dashboard/ProjectFinancialSummaryV2", | |||
| // isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => | |||
| // abilities!.includes(ability), | |||
| // ), | |||
| // showOnMobile: true, | |||
| // }, | |||
| // No Claim function in Breaur, will be implement later | |||
| // { | |||
| // icon: <RequestQuote />, | |||
| @@ -1,6 +1,6 @@ | |||
| "use client"; | |||
| import React, { useEffect } from "react"; | |||
| import React, { useCallback, useEffect, useState } from "react"; | |||
| import Paper from "@mui/material/Paper"; | |||
| import Table from "@mui/material/Table"; | |||
| import TableBody from "@mui/material/TableBody"; | |||
| @@ -18,6 +18,9 @@ import { convertDateArrayToString, moneyFormatter } from "@/app/utils/formatUtil | |||
| import DoneIcon from '@mui/icons-material/Done'; | |||
| import CloseIcon from '@mui/icons-material/Close'; | |||
| import { Button, Link, LinkOwnProps } from "@mui/material"; | |||
| import { orderBy, sortBy } from "lodash"; | |||
| import ArrowUp from '@mui/icons-material/KeyboardArrowUp'; | |||
| import ArrowDown from '@mui/icons-material/KeyboardArrowDown'; | |||
| export interface ResultWithId { | |||
| id: string | number; | |||
| @@ -155,7 +158,50 @@ function SearchResults<T extends ResultWithId>({ | |||
| return column.underline ?? "always"; | |||
| }; | |||
| type OrderProps = Record<keyof T, Boolean> | |||
| const [sortedItems, setSortedItems] = useState(items) | |||
| const [orderProps, setOrderProps] = useState<OrderProps>(() => { | |||
| if (items.length === 0) { | |||
| return {} as OrderProps | |||
| } | |||
| return Object.keys(sortedItems[0]).reduce((acc, key) => { | |||
| if (key === "deleted" || key === "id") return acc | |||
| acc[key as keyof T] = false; | |||
| return acc; | |||
| }, {} as OrderProps); | |||
| }); | |||
| const changeOrder = useCallback((key: keyof T) => { | |||
| // preserve all column sorting | |||
| // setOrderProps( | |||
| // (prev) => ({ | |||
| // [...prev]: false, | |||
| // [key]: !prev[key] | |||
| // }) | |||
| // ) | |||
| // only sort 1 column | |||
| setOrderProps( | |||
| (prev) => { | |||
| const newOrderProps = Object.keys(prev).reduce((acc, currKey) => { | |||
| acc[currKey as keyof T] = currKey === key ? !prev[currKey as keyof T] : false; | |||
| return acc; | |||
| }, {} as OrderProps); | |||
| return newOrderProps; | |||
| } | |||
| ) | |||
| }, []) | |||
| const sortingItems = useCallback( | |||
| (key: keyof T) => { | |||
| // true === asc | |||
| // false === desc | |||
| console.log(orderProps) | |||
| if (orderProps[key]) { | |||
| return orderBy(sortedItems, key, 'asc') | |||
| } else { | |||
| return orderBy(sortedItems, key, 'desc') | |||
| } | |||
| } | |||
| , [sortedItems, orderProps] | |||
| ) | |||
| const table = ( | |||
| <> | |||
| <TableContainer sx={{ maxHeight: 440 }}> | |||
| @@ -163,14 +209,25 @@ function SearchResults<T extends ResultWithId>({ | |||
| <TableHead> | |||
| <TableRow> | |||
| {columns.filter(item => item.isHidden !== true).map((column, idx) => ( | |||
| <TableCell key={`${column.name.toString()}${idx}`}> | |||
| <TableCell | |||
| key={`${column.name.toString()}${idx}`} | |||
| onClick={() => { | |||
| changeOrder(column.name) | |||
| setSortedItems(sortingItems(column.name)) | |||
| }} | |||
| > | |||
| {column?.type === "money" ? <div style={{display: "flex", justifyContent: "flex-end"}}>{column.label}</div> : column.label} | |||
| {(() => { | |||
| const isAscending = orderProps[column.name]; | |||
| if (isAscending === undefined) return undefined; | |||
| return isAscending ? <ArrowUp /> : <ArrowDown />; | |||
| })()} | |||
| </TableCell> | |||
| ))} | |||
| </TableRow> | |||
| </TableHead> | |||
| <TableBody> | |||
| {items | |||
| {sortedItems | |||
| .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) | |||
| .map((item) => { | |||
| return ( | |||
| @@ -223,7 +280,7 @@ function SearchResults<T extends ResultWithId>({ | |||
| <TablePagination | |||
| rowsPerPageOptions={[10, 25, 100]} | |||
| component="div" | |||
| count={items.length} | |||
| count={sortedItems.length} | |||
| rowsPerPage={rowsPerPage} | |||
| page={page} | |||
| onPageChange={handleChangePage} | |||
| @@ -176,6 +176,19 @@ export const PW_RULE_THEME = createTheme({ | |||
| }, | |||
| }); | |||
| export const SMALL_ROW_THEME = createTheme({ | |||
| components: { | |||
| MuiTableRow: { | |||
| styleOverrides: { | |||
| root: { | |||
| // padding: "0px", | |||
| height: "0px" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| }) | |||
| export const TSMS_BUTTON_THEME = createTheme({ | |||
| palette: { | |||
| primary: { | |||