@@ -47,6 +47,7 @@ | |||||
"react-dom": "^18", | "react-dom": "^18", | ||||
"react-hook-form": "^7.49.2", | "react-hook-form": "^7.49.2", | ||||
"react-i18next": "^13.5.0", | "react-i18next": "^13.5.0", | ||||
"react-idle-timer": "^5.7.2", | |||||
"react-intl": "^6.5.5", | "react-intl": "^6.5.5", | ||||
"react-number-format": "^5.3.4", | "react-number-format": "^5.3.4", | ||||
"react-select": "^5.8.0", | "react-select": "^5.8.0", | ||||
@@ -9043,6 +9044,15 @@ | |||||
} | } | ||||
} | } | ||||
}, | }, | ||||
"node_modules/react-idle-timer": { | |||||
"version": "5.7.2", | |||||
"resolved": "https://registry.npmjs.org/react-idle-timer/-/react-idle-timer-5.7.2.tgz", | |||||
"integrity": "sha512-+BaPfc7XEUU5JFkwZCx6fO1bLVK+RBlFH+iY4X34urvIzZiZINP6v2orePx3E6pAztJGE7t4DzvL7if2SL/0GQ==", | |||||
"peerDependencies": { | |||||
"react": ">=16", | |||||
"react-dom": ">=16" | |||||
} | |||||
}, | |||||
"node_modules/react-intl": { | "node_modules/react-intl": { | ||||
"version": "6.6.2", | "version": "6.6.2", | ||||
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.6.2.tgz", | "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-6.6.2.tgz", | ||||
@@ -21,6 +21,33 @@ export const OUTPUT_DATE_FORMAT = "YYYY/MM/DD"; | |||||
export const OUTPUT_TIME_FORMAT = "HH:mm:ss"; | export const OUTPUT_TIME_FORMAT = "HH:mm:ss"; | ||||
export const truncateMoney = (amount: number | undefined) => { | |||||
if (!amount) { | |||||
return amount; | |||||
} | |||||
const { maximumFractionDigits, minimumFractionDigits } = | |||||
moneyFormatter.resolvedOptions(); | |||||
const fractionDigits = maximumFractionDigits ?? minimumFractionDigits ?? 0; | |||||
const factor = Math.pow(10, fractionDigits); | |||||
const truncatedAmount = Math.floor(amount * factor) / factor; | |||||
return truncatedAmount; | |||||
}; | |||||
export const sumMoney = (a: number, b: number) => { | |||||
const { maximumFractionDigits, minimumFractionDigits } = | |||||
moneyFormatter.resolvedOptions(); | |||||
const fractionDigits = maximumFractionDigits ?? minimumFractionDigits ?? 0; | |||||
const factor = Math.pow(10, fractionDigits); | |||||
const sum = Math.round(a * factor) + Math.round(b * factor); | |||||
return sum / factor; | |||||
}; | |||||
export const convertDateToString = ( | export const convertDateToString = ( | ||||
date: Date, | date: Date, | ||||
format: string = OUTPUT_DATE_FORMAT, | format: string = OUTPUT_DATE_FORMAT, | ||||
@@ -33,8 +60,8 @@ export const convertDateArrayToString = ( | |||||
format: string = OUTPUT_DATE_FORMAT, | format: string = OUTPUT_DATE_FORMAT, | ||||
needTime: boolean = false, | needTime: boolean = false, | ||||
) => { | ) => { | ||||
if (dateArray === null){ | |||||
return "-" | |||||
if (dateArray === null) { | |||||
return "-"; | |||||
} | } | ||||
if (dateArray.length === 6) { | if (dateArray.length === 6) { | ||||
if (!needTime) { | if (!needTime) { | ||||
@@ -48,8 +75,8 @@ export const convertDateArrayToString = ( | |||||
return dayjs(dateString).format(format); | return dayjs(dateString).format(format); | ||||
} | } | ||||
} | } | ||||
if (dateArray.length === 0){ | |||||
return "-" | |||||
if (dateArray.length === 0) { | |||||
return "-"; | |||||
} | } | ||||
}; | }; | ||||
@@ -134,8 +161,8 @@ export function convertLocaleStringToNumber(numberString: string): number { | |||||
} | } | ||||
export function timestampToDateString(timestamp: string): string { | export function timestampToDateString(timestamp: string): string { | ||||
if (timestamp === null){ | |||||
return "-" | |||||
if (timestamp === null) { | |||||
return "-"; | |||||
} | } | ||||
const date = new Date(timestamp); | const date = new Date(timestamp); | ||||
const year = date.getFullYear(); | const year = date.getFullYear(); | ||||
@@ -26,6 +26,7 @@ import { | |||||
INPUT_DATE_FORMAT, | INPUT_DATE_FORMAT, | ||||
moneyFormatter, | moneyFormatter, | ||||
OUTPUT_DATE_FORMAT, | OUTPUT_DATE_FORMAT, | ||||
truncateMoney, | |||||
} from "@/app/utils/formatUtil"; | } from "@/app/utils/formatUtil"; | ||||
import { PaymentInputs } from "@/app/api/projects/actions"; | import { PaymentInputs } from "@/app/api/projects/actions"; | ||||
import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||
@@ -94,7 +95,7 @@ const BulkAddPaymentModal: React.FC<Props> = ({ | |||||
amountToDivide && | amountToDivide && | ||||
description | description | ||||
) { | ) { | ||||
const dividedAmount = amountToDivide / numberOfEntries; | |||||
const dividedAmount = truncateMoney(amountToDivide / numberOfEntries)!; | |||||
return Array(numberOfEntries) | return Array(numberOfEntries) | ||||
.fill(undefined) | .fill(undefined) | ||||
.map((_, index) => { | .map((_, index) => { | ||||
@@ -35,6 +35,7 @@ import { | |||||
INPUT_DATE_FORMAT, | INPUT_DATE_FORMAT, | ||||
OUTPUT_DATE_FORMAT, | OUTPUT_DATE_FORMAT, | ||||
moneyFormatter, | moneyFormatter, | ||||
truncateMoney, | |||||
} from "@/app/utils/formatUtil"; | } from "@/app/utils/formatUtil"; | ||||
import isDate from "lodash/isDate"; | import isDate from "lodash/isDate"; | ||||
import BulkAddPaymentModal from "./BulkAddPaymentModal"; | import BulkAddPaymentModal from "./BulkAddPaymentModal"; | ||||
@@ -148,7 +149,11 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars | // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||||
const { _isNew, _errors, ...updatedRow } = newRow; | const { _isNew, _errors, ...updatedRow } = newRow; | ||||
setPayments((ps) => | setPayments((ps) => | ||||
ps.map((p) => (p.id === updatedRow.id ? updatedRow : p)), | |||||
ps.map((p) => | |||||
p.id === updatedRow.id | |||||
? { ...updatedRow, amount: truncateMoney(updatedRow.amount) } | |||||
: p, | |||||
), | |||||
); | ); | ||||
return updatedRow; | return updatedRow; | ||||
}, | }, | ||||
@@ -246,6 +251,9 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||||
width: 300, | width: 300, | ||||
editable: true, | editable: true, | ||||
type: "number", | type: "number", | ||||
valueGetter(params) { | |||||
return truncateMoney(params.value); | |||||
}, | |||||
valueFormatter(params) { | valueFormatter(params) { | ||||
return moneyFormatter.format(params.value); | return moneyFormatter.format(params.value); | ||||
}, | }, | ||||
@@ -1,6 +1,6 @@ | |||||
import { CreateProjectInputs } from "@/app/api/projects/actions"; | import { CreateProjectInputs } from "@/app/api/projects/actions"; | ||||
import { TaskGroup } from "@/app/api/tasks"; | import { TaskGroup } from "@/app/api/tasks"; | ||||
import { moneyFormatter } from "@/app/utils/formatUtil"; | |||||
import { moneyFormatter, sumMoney } from "@/app/utils/formatUtil"; | |||||
import { | import { | ||||
Button, | Button, | ||||
Divider, | Divider, | ||||
@@ -52,9 +52,12 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { | |||||
<Stack spacing={1}> | <Stack spacing={1}> | ||||
{taskGroups.map((group, index) => { | {taskGroups.map((group, index) => { | ||||
const payments = milestones[group.id]?.payments || []; | const payments = milestones[group.id]?.payments || []; | ||||
const paymentTotal = payments.reduce((acc, p) => acc + p.amount, 0); | |||||
const paymentTotal = payments.reduce( | |||||
(acc, p) => sumMoney(acc, p.amount), | |||||
0, | |||||
); | |||||
projectTotal += paymentTotal; | |||||
projectTotal = sumMoney(projectTotal, paymentTotal); | |||||
return ( | return ( | ||||
<Stack | <Stack | ||||