#12 develop

已合併
jason.tom 4 月之前 將 4 次代碼提交從 develop合併至 main
  1. +2
    -2
      src/app/api/invoices/actions.ts
  2. +1
    -1
      src/app/utils/formatUtil.ts
  3. +34
    -20
      src/components/CreateProject/CreateProject.tsx
  4. +1
    -1
      src/components/CreateProject/CreateProjectWrapper.tsx
  5. +10
    -2
      src/components/CreateProject/Milestone.tsx
  6. +4
    -4
      src/components/CreateProject/ProjectClientDetails.tsx
  7. +1
    -1
      src/components/CreateStaff/StaffInfo.tsx
  8. +1
    -1
      src/components/InvoiceSearch/CreateInvoiceModal.tsx
  9. +10
    -6
      src/components/InvoiceSearch/InvoiceSearch.tsx
  10. +3
    -0
      src/components/LeaveTable/LeaveTable.tsx
  11. +10
    -11
      src/components/NavigationContent/NavigationContent.tsx
  12. +62
    -5
      src/components/SearchResults/SearchResults.tsx
  13. +13
    -0
      src/theme/colorConst.js

+ 2
- 2
src/app/api/invoices/actions.ts 查看文件

@@ -2,7 +2,7 @@


import { serverFetchJson, serverFetchString, serverFetchWithNoContent } from "@/app/utils/fetchUtil"; import { serverFetchJson, serverFetchString, serverFetchWithNoContent } from "@/app/utils/fetchUtil";
import { BASE_API_URL } from "@/config/api"; import { BASE_API_URL } from "@/config/api";
import { revalidateTag } from "next/cache";
import { revalidatePath, revalidateTag } from "next/cache";
import { cache } from "react"; import { cache } from "react";


export interface InvoiceResult { export interface InvoiceResult {
@@ -145,8 +145,8 @@ export const updateInvoice = async (data: any) => {
body: JSON.stringify(data), body: JSON.stringify(data),
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
revalidateTag("invoices") revalidateTag("invoices")
revalidatePath("/(main)/invoice")
return updateInvoice; return updateInvoice;
} }




+ 1
- 1
src/app/utils/formatUtil.ts 查看文件

@@ -32,7 +32,7 @@ export const truncateMoney = (amount: number | undefined) => {
const fractionDigits = maximumFractionDigits ?? minimumFractionDigits ?? 0; const fractionDigits = maximumFractionDigits ?? minimumFractionDigits ?? 0;


const factor = Math.pow(10, fractionDigits); const factor = Math.pow(10, fractionDigits);
const truncatedAmount = Math.floor(amount * factor) / factor;
const truncatedAmount = Math.round(amount * factor) / factor;


return truncatedAmount; return truncatedAmount;
}; };


+ 34
- 20
src/components/CreateProject/CreateProject.tsx 查看文件

@@ -59,7 +59,7 @@ import {
} from "../Swal/CustomAlerts"; } from "../Swal/CustomAlerts";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { DELETE_PROJECT } from "@/middleware"; 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"; import { deleteDraft, loadDraft, saveToLocalStorage } from "@/app/utils/draftUtils";


export interface Props { export interface Props {
@@ -273,7 +273,7 @@ const CreateProject: React.FC<Props> = ({
const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>( const onSubmit = useCallback<SubmitHandler<CreateProjectInputs>>(
async (data, event) => { async (data, event) => {
try { try {
console.log(data);
// console.log(data);


// detect errors // detect errors
let hasErrors = false; let hasErrors = false;
@@ -374,30 +374,43 @@ const CreateProject: React.FC<Props> = ({
data.milestones[parseFloat(key)].startDate = null data.milestones[parseFloat(key)].startDate = null
data.milestones[parseFloat(key)].endDate = 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( projectTotal += payments.reduce(
(acc, payment) => acc + payment.amount, (acc, payment) => acc + payment.amount,
0, 0,
); );
**/

projectTotal += payments.reduce(
(acc, p) => sumMoney(acc, p.amount),
0,
);

}); });
// console.log(projectTotal)
console.log(milestonesKeys)


if ( if (
projectTotal !== data.expectedProjectFee ||
milestonesKeys.length !== taskGroupKeys.length
projectTotal !== data.expectedProjectFee
// || milestonesKeys.length !== taskGroupKeys.length
) { ) {
formProps.setError("milestones", { formProps.setError("milestones", {
message: "milestones is not valid", message: "milestones is not valid",
@@ -484,7 +497,7 @@ const CreateProject: React.FC<Props> = ({
setServerError(t("An error has occurred. Please try again later.")); setServerError(t("An error has occurred. Please try again later."));
} }
}, },
[router, t],
[router, t, draftId],
); );


const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>( const onSubmitError = useCallback<SubmitErrorHandler<CreateProjectInputs>>(
@@ -565,6 +578,7 @@ const CreateProject: React.FC<Props> = ({


const saveDraft = useCallback(async () => { const saveDraft = useCallback(async () => {
const currentTimestamp = Date.now() const currentTimestamp = Date.now()
console.log(currentTimestamp)


saveToLocalStorage(draftId || currentTimestamp, formProps.getValues()); saveToLocalStorage(draftId || currentTimestamp, formProps.getValues());


+ 1
- 1
src/components/CreateProject/CreateProjectWrapper.tsx 查看文件

@@ -83,7 +83,7 @@ const CreateProjectWrapper: React.FC<Props> = async (props) => {
var filteredTeamLeads = teamId ? teamLeads.filter( var filteredTeamLeads = teamId ? teamLeads.filter(
(teamLead) => teamLead.teamId === teamId, (teamLead) => teamLead.teamId === teamId,
) : teamLeads ) : teamLeads
if (userStaff?.id !== null && userStaff?.id == 1) {
if (userStaff?.id != null && userStaff?.id == 1) {
filteredTeamLeads = teamLeads.filter( filteredTeamLeads = teamLeads.filter(
(teamLead) => teamLead.teamId === teamId || teamLead.team == "ST", (teamLead) => teamLead.teamId === teamId || teamLead.team == "ST",
) )


+ 10
- 2
src/components/CreateProject/Milestone.tsx 查看文件

@@ -23,6 +23,7 @@ import { useFormContext } from "react-hook-form";
import { CreateProjectInputs } from "@/app/api/projects/actions"; import { CreateProjectInputs } from "@/app/api/projects/actions";
import MilestoneSection from "./MilestoneSection"; import MilestoneSection from "./MilestoneSection";
import ProjectTotalFee from "./ProjectTotalFee"; import ProjectTotalFee from "./ProjectTotalFee";
import { sumMoney } from "@/app/utils/formatUtil";


export interface Props { export interface Props {
allTasks: Task[]; allTasks: Task[];
@@ -89,10 +90,17 @@ const Milestone: React.FC<Props> = ({ allTasks, isActive }) => {
// hasError = true // 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 hasError = true
} }
// console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0)) // console.log(Object.keys(milestones).reduce((acc, key) => acc + milestones[parseFloat(key)].payments.reduce((acc2, value) => acc2 + value.amount, 0), 0))


+ 4
- 4
src/components/CreateProject/ProjectClientDetails.tsx 查看文件

@@ -242,18 +242,18 @@ const ProjectClientDetails: React.FC<Props> = ({
// (acc, wn) => ({ ...acc, [wn.id]: wn.name }), // (acc, wn) => ({ ...acc, [wn.id]: wn.name }),
// {}, // {},
// ); // );
const planStart = getValues("projectPlanStart")
const planEnd = getValues("projectPlanEnd")
const planStart = watch("projectPlanStart")
const planEnd = watch("projectPlanEnd")


useEffect(() => { useEffect(() => {
let hasErrors = false let hasErrors = false
if( if(
!planStart || planStart > planEnd
!planStart || new Date(planStart) > new Date(planEnd)
){ ){
hasErrors = true; hasErrors = true;
} }
if( if(
!planEnd || planStart > planEnd
!planEnd || new Date(planStart) > new Date(planEnd)
){ ){
hasErrors = true; hasErrors = true;
} }


+ 1
- 1
src/components/CreateStaff/StaffInfo.tsx 查看文件

@@ -26,7 +26,7 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { DemoItem } from "@mui/x-date-pickers/internals/demo"; import { DemoItem } from "@mui/x-date-pickers/internals/demo";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil"; import { INPUT_DATE_FORMAT } from "@/app/utils/formatUtil";
import TableModal from "./TableModal";
// import TableModal from "./TableModal";
import { Preview } from "@mui/icons-material"; import { Preview } from "@mui/icons-material";
import SalaryEffectiveModel from "../EditStaff/SalaryEffectiveModel"; import SalaryEffectiveModel from "../EditStaff/SalaryEffectiveModel";
import TeamHistoryModal from "../EditStaff/TeamHistoryModal"; import TeamHistoryModal from "../EditStaff/TeamHistoryModal";


+ 1
- 1
src/components/InvoiceSearch/CreateInvoiceModal.tsx 查看文件

@@ -69,7 +69,7 @@ const CreateInvoiceModal: React.FC<Props> = ({isOpen, onClose, projects, invoice
if (response === "OK") { if (response === "OK") {
onClose() onClose()
successDialog(t("Submit Success"), t).then(() => { successDialog(t("Submit Success"), t).then(() => {
router.replace("/invoice");
// router.replace("/invoice");
}) })
} }
}, t) }, t)


+ 10
- 6
src/components/InvoiceSearch/InvoiceSearch.tsx 查看文件

@@ -6,7 +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 { moneyFormatter } from "@/app/utils/formatUtil" 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 FileUploadIcon from '@mui/icons-material/FileUpload';
import AddIcon from '@mui/icons-material/Add'; import AddIcon from '@mui/icons-material/Add';
import { deleteInvoice, importIssuedInovice, importReceivedInovice, updateInvoice } from "@/app/api/invoices/actions"; 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 { IMPORT_INVOICE, IMPORT_RECEIPT } from "@/middleware";
import InvoiceSearchLoading from "./InvoiceSearchLoading"; import InvoiceSearchLoading from "./InvoiceSearchLoading";
import { NumberFormatValues, NumericFormat } from "react-number-format"; import { NumberFormatValues, NumericFormat } from "react-number-format";
import theme from "@/theme";
import { SMALL_ROW_THEME } from "@/theme/colorConst";


interface CustomMoneyComponentProps { interface CustomMoneyComponentProps {
params: GridRenderEditCellParams; params: GridRenderEditCellParams;
@@ -614,11 +616,13 @@ const InvoiceSearch: React.FC<Props> & SubComponents = ({ invoices, projects, ab
{ {
!loading && !loading &&
// tabIndex == 0 && // tabIndex == 0 &&
<SearchResults<invoiceList>
items={filteredIvoices}
columns={combinedColumns}
autoRedirectToFirstPage
/>
<ThemeProvider theme={SMALL_ROW_THEME}>
<SearchResults<invoiceList>
items={filteredIvoices}
columns={combinedColumns}
autoRedirectToFirstPage
/>
</ThemeProvider>
} }
<Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="lg" fullWidth> <Dialog open={dialogOpen} onClose={handleCloseDialog} maxWidth="lg" fullWidth>
<DialogTitle>{t("Edit Invoice")}</DialogTitle> <DialogTitle>{t("Edit Invoice")}</DialogTitle>


+ 3
- 0
src/components/LeaveTable/LeaveTable.tsx 查看文件

@@ -13,12 +13,14 @@ interface Props {
leaveTypes: LeaveType[]; leaveTypes: LeaveType[];
timesheetRecords: RecordTimesheetInput; timesheetRecords: RecordTimesheetInput;
companyHolidays: HolidaysResult[]; companyHolidays: HolidaysResult[];
isSaturdayWorker: boolean;
} }


const LeaveTable: React.FC<Props> = ({ const LeaveTable: React.FC<Props> = ({
leaveTypes, leaveTypes,
timesheetRecords, timesheetRecords,
companyHolidays, companyHolidays,
isSaturdayWorker
}) => { }) => {
const { watch } = useFormContext<RecordLeaveInput>(); const { watch } = useFormContext<RecordLeaveInput>();
const currentInput = watch(); const currentInput = watch();
@@ -32,6 +34,7 @@ const LeaveTable: React.FC<Props> = ({
timesheetEntries={timesheetRecords} timesheetEntries={timesheetRecords}
EntryTableComponent={LeaveEntryTable} EntryTableComponent={LeaveEntryTable}
entryTableProps={{ leaveTypes }} entryTableProps={{ leaveTypes }}
isSaturdayWorker={isSaturdayWorker}
/> />
); );
}; };


+ 10
- 11
src/components/NavigationContent/NavigationContent.tsx 查看文件

@@ -110,10 +110,19 @@ const NavigationContent: React.FC<Props> = ({ abilities, username }) => {
path: "/home", path: "/home",
showOnMobile: true, 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 />, icon: <SummarizeIcon />,
label: "Financial Summary", label: "Financial Summary",
path: "/dashboard/ProjectFinancialSummary",
path: "/dashboard/ProjectFinancialSummaryV2",
isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) => isHidden: ![VIEW_DASHBOARD_ALL, VIEW_DASHBOARD_SELF].some((ability) =>
abilities!.includes(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 // No Claim function in Breaur, will be implement later
// { // {
// icon: <RequestQuote />, // icon: <RequestQuote />,


+ 62
- 5
src/components/SearchResults/SearchResults.tsx 查看文件

@@ -1,6 +1,6 @@
"use client"; "use client";


import React, { useEffect } from "react";
import React, { useCallback, useEffect, useState } from "react";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table"; import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody"; 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 DoneIcon from '@mui/icons-material/Done';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import { Button, Link, LinkOwnProps } from "@mui/material"; 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 { export interface ResultWithId {
id: string | number; id: string | number;
@@ -155,7 +158,50 @@ function SearchResults<T extends ResultWithId>({


return column.underline ?? "always"; 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 = ( const table = (
<> <>
<TableContainer sx={{ maxHeight: 440 }}> <TableContainer sx={{ maxHeight: 440 }}>
@@ -163,14 +209,25 @@ function SearchResults<T extends ResultWithId>({
<TableHead> <TableHead>
<TableRow> <TableRow>
{columns.filter(item => item.isHidden !== true).map((column, idx) => ( {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} {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> </TableCell>
))} ))}
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{items
{sortedItems
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((item) => { .map((item) => {
return ( return (
@@ -223,7 +280,7 @@ function SearchResults<T extends ResultWithId>({
<TablePagination <TablePagination
rowsPerPageOptions={[10, 25, 100]} rowsPerPageOptions={[10, 25, 100]}
component="div" component="div"
count={items.length}
count={sortedItems.length}
rowsPerPage={rowsPerPage} rowsPerPage={rowsPerPage}
page={page} page={page}
onPageChange={handleChangePage} onPageChange={handleChangePage}


+ 13
- 0
src/theme/colorConst.js 查看文件

@@ -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({ export const TSMS_BUTTON_THEME = createTheme({
palette: { palette: {
primary: { primary: {


Loading…
取消
儲存