| @@ -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 | ||||