| @@ -47,6 +47,7 @@ | |||
| "react-dom": "^18", | |||
| "react-hook-form": "^7.49.2", | |||
| "react-i18next": "^13.5.0", | |||
| "react-idle-timer": "^5.7.2", | |||
| "react-intl": "^6.5.5", | |||
| "react-number-format": "^5.3.4", | |||
| "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": { | |||
| "version": "6.6.2", | |||
| "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 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 = ( | |||
| date: Date, | |||
| format: string = OUTPUT_DATE_FORMAT, | |||
| @@ -33,8 +60,8 @@ export const convertDateArrayToString = ( | |||
| format: string = OUTPUT_DATE_FORMAT, | |||
| needTime: boolean = false, | |||
| ) => { | |||
| if (dateArray === null){ | |||
| return "-" | |||
| if (dateArray === null) { | |||
| return "-"; | |||
| } | |||
| if (dateArray.length === 6) { | |||
| if (!needTime) { | |||
| @@ -48,8 +75,8 @@ export const convertDateArrayToString = ( | |||
| 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 { | |||
| if (timestamp === null){ | |||
| return "-" | |||
| if (timestamp === null) { | |||
| return "-"; | |||
| } | |||
| const date = new Date(timestamp); | |||
| const year = date.getFullYear(); | |||
| @@ -26,6 +26,7 @@ import { | |||
| INPUT_DATE_FORMAT, | |||
| moneyFormatter, | |||
| OUTPUT_DATE_FORMAT, | |||
| truncateMoney, | |||
| } from "@/app/utils/formatUtil"; | |||
| import { PaymentInputs } from "@/app/api/projects/actions"; | |||
| import dayjs from "dayjs"; | |||
| @@ -94,7 +95,7 @@ const BulkAddPaymentModal: React.FC<Props> = ({ | |||
| amountToDivide && | |||
| description | |||
| ) { | |||
| const dividedAmount = amountToDivide / numberOfEntries; | |||
| const dividedAmount = truncateMoney(amountToDivide / numberOfEntries)!; | |||
| return Array(numberOfEntries) | |||
| .fill(undefined) | |||
| .map((_, index) => { | |||
| @@ -35,6 +35,7 @@ import { | |||
| INPUT_DATE_FORMAT, | |||
| OUTPUT_DATE_FORMAT, | |||
| moneyFormatter, | |||
| truncateMoney, | |||
| } from "@/app/utils/formatUtil"; | |||
| import isDate from "lodash/isDate"; | |||
| import BulkAddPaymentModal from "./BulkAddPaymentModal"; | |||
| @@ -148,7 +149,11 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | |||
| const { _isNew, _errors, ...updatedRow } = newRow; | |||
| 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; | |||
| }, | |||
| @@ -246,6 +251,9 @@ const MilestoneSection: React.FC<Props> = ({ taskGroupId }) => { | |||
| width: 300, | |||
| editable: true, | |||
| type: "number", | |||
| valueGetter(params) { | |||
| return truncateMoney(params.value); | |||
| }, | |||
| valueFormatter(params) { | |||
| return moneyFormatter.format(params.value); | |||
| }, | |||
| @@ -1,6 +1,6 @@ | |||
| import { CreateProjectInputs } from "@/app/api/projects/actions"; | |||
| import { TaskGroup } from "@/app/api/tasks"; | |||
| import { moneyFormatter } from "@/app/utils/formatUtil"; | |||
| import { moneyFormatter, sumMoney } from "@/app/utils/formatUtil"; | |||
| import { | |||
| Button, | |||
| Divider, | |||
| @@ -52,9 +52,12 @@ const ProjectTotalFee: React.FC<Props> = ({ taskGroups }) => { | |||
| <Stack spacing={1}> | |||
| {taskGroups.map((group, index) => { | |||
| 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 ( | |||
| <Stack | |||