From 36d7e712e2def465a173fe114ea005eda15cf8d8 Mon Sep 17 00:00:00 2001 From: julian Date: Tue, 1 Oct 2024 19:14:46 +0800 Subject: [PATCH] Feat: develop voucher line --- package.json | 2 +- src/components/voucher/new_voucher_form.tsx | 13 +- src/components/voucher/voucher_line_block.tsx | 291 ++++++++++++++++-- 3 files changed, 277 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index e7b01441..8d10b018 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iSunFA", - "version": "0.8.2+23", + "version": "0.8.2+24", "private": false, "scripts": { "dev": "next dev", diff --git a/src/components/voucher/new_voucher_form.tsx b/src/components/voucher/new_voucher_form.tsx index a30e0179..8cd2983b 100644 --- a/src/components/voucher/new_voucher_form.tsx +++ b/src/components/voucher/new_voucher_form.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { FaChevronDown } from 'react-icons/fa6'; import { BiSave } from 'react-icons/bi'; import { useTranslation } from 'next-i18next'; @@ -10,16 +10,27 @@ import { IDatePeriod } from '@/interfaces/date_period'; import { default30DayPeriodInSec } from '@/constants/display'; import { VoucherType } from '@/constants/account'; import VoucherLineBlock from '@/components/voucher/voucher_line_block'; +import { useUserCtx } from '@/contexts/user_context'; +import { useAccountingCtx } from '@/contexts/accounting_context'; const NewVoucherForm = () => { const { t } = useTranslation('common'); + const { selectedCompany } = useUserCtx(); + const { getAccountListHandler } = useAccountingCtx(); + const [date, setDate] = useState(default30DayPeriodInSec); const [type, setType] = useState(VoucherType.EXPENSE); const [note, setNote] = useState(''); const [counterparty, setCounterparty] = useState(''); const [isRecurring, setIsRecurring] = useState(false); + useEffect(() => { + if (selectedCompany) { + getAccountListHandler(selectedCompany.id); + } + }, [selectedCompany]); + // ToDo: (20240926 - Julian) Add 'credit not equal to debit' const saveBtnDisabled = (date.startTimeStamp === 0 && date.endTimeStamp === 0) || type === ''; diff --git a/src/components/voucher/voucher_line_block.tsx b/src/components/voucher/voucher_line_block.tsx index f992e8ad..9bb2b482 100644 --- a/src/components/voucher/voucher_line_block.tsx +++ b/src/components/voucher/voucher_line_block.tsx @@ -1,58 +1,211 @@ -import React, { useState } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { FaPlus } from 'react-icons/fa6'; import { FiBookOpen } from 'react-icons/fi'; import { LuTrash2 } from 'react-icons/lu'; import { Button } from '@/components/button/button'; import { numberWithCommas } from '@/lib/utils/common'; +import { useAccountingCtx } from '@/contexts/accounting_context'; +import { IAccount } from '@/interfaces/accounting_account'; +import useOuterClick from '@/lib/hooks/use_outer_click'; + +interface ILineItem { + id: number; + account: IAccount | null; + particulars: string; + debit: number; + credit: number; +} + +const VoucherLineItem = ({ + deleteHandler, + accountTitleHandler, + particularsChangeHandler, + debitChangeHandler, + creditChangeHandler, +}: { + deleteHandler: () => void; + accountTitleHandler: (account: IAccount | null) => void; + particularsChangeHandler: (particulars: string) => void; + debitChangeHandler: (debit: number) => void; + creditChangeHandler: (credit: number) => void; +}) => { + const { accountList } = useAccountingCtx(); + + const [accountTitle, setAccountTitle] = useState('Accounting'); + const [searchKeyword, setSearchKeyword] = useState(''); + const [filteredAccountList, setFilteredAccountList] = useState(accountList); -const VoucherLineItem = () => { - // ToDo: (20240927 - Julian) Implement accounting - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [accounting, setAccounting] = useState(''); const [particulars, setParticulars] = useState(''); - const [debit, setDebit] = useState(0); - const [credit, setCredit] = useState(0); + const [debitInput, setDebitInput] = useState(''); + const [creditInput, setCreditInput] = useState(''); + + const accountInputRef = useRef(null); - const particularsChangeHandler = (e: React.ChangeEvent) => { + const { + targetRef: accountingRef, + componentVisible: isAccountingMenuOpen, + setComponentVisible: setAccountingMenuOpen, + } = useOuterClick(false); + + const { + targetRef: accountRef, + componentVisible: isAccountEditing, + setComponentVisible: setIsAccountEditing, + } = useOuterClick(false); + + // Info: (20241001 - Julian) 搜尋 Account + useEffect(() => { + const filteredList = accountList.filter((account) => { + // Info: (20241001 - Julian) 編號(數字)搜尋: 字首符合 + if (searchKeyword.match(/^\d+$/)) { + const codeMatch = account.code.toLowerCase().startsWith(searchKeyword.toLowerCase()); + return codeMatch; + } else if (searchKeyword !== '') { + // Info: (20241001 - Julian) 名稱搜尋: 部分符合 + const nameMatch = account.name.toLowerCase().includes(searchKeyword.toLowerCase()); + return nameMatch; + } + return true; + }); + setFilteredAccountList(filteredList); + }, [searchKeyword, accountList]); + + const isDebitDisabled = creditInput !== ''; + const isCreditDisabled = debitInput !== ''; + + const accountSearchHandler = (e: React.ChangeEvent) => { + setSearchKeyword(e.target.value); + setAccountingMenuOpen(true); + }; + + const particularsInputChangeHandler = (e: React.ChangeEvent) => { setParticulars(e.target.value); + particularsChangeHandler(e.target.value); }; - const creditChangeHandler = (e: React.ChangeEvent) => { - setCredit(Number(e.target.value)); + + const debitInputChangeHandler = (e: React.ChangeEvent) => { + // Info: (20241001 - Julian) 限制只能輸入數字 + const debitValue = e.target.value.replace(/\D/g, ''); + // Info: (20241001 - Julian) 加入千分位逗號 + setDebitInput(numberWithCommas(debitValue)); + // Info: (20241001 - Julian) 設定 Debit + debitChangeHandler(Number(debitValue)); }; - const debitChangeHandler = (e: React.ChangeEvent) => { - setDebit(Number(e.target.value)); + + const creditInputChangeHandler = (e: React.ChangeEvent) => { + // Info: (20241001 - Julian) 限制只能輸入數字 + const creditValue = e.target.value.replace(/\D/g, ''); + // Info: (20241001 - Julian) 加入千分位逗號 + setCreditInput(numberWithCommas(creditValue)); + // Info: (20241001 - Julian) 設定 Credit + creditChangeHandler(Number(creditValue)); + }; + + const accountEditingHandler = () => { + setIsAccountEditing(true); + setAccountingMenuOpen(true); + // Info: (20241001 - Julian) Focus on input + if (accountInputRef.current) { + accountInputRef.current.focus(); + } }; + const accountingMenu = + filteredAccountList.length > 0 ? ( + filteredAccountList.map((account) => { + const accountClickHandler = () => { + setAccountTitle(`${account.code} ${account.name}`); + // Info: (20241001 - Julian) 關閉 Accounting Menu 和編輯狀態 + setAccountingMenuOpen(false); + setIsAccountEditing(false); + // Info: (20241001 - Julian) 重置搜尋關鍵字 + setSearchKeyword(''); + // Info: (20241001 - Julian) 設定 Account title + accountTitleHandler(account); + }; + + return ( + + ); + }) + ) : ( +

Loading...

+ ); + + const displayedAccountingMenu = isAccountingMenuOpen ? ( +
+

+ assets +

+
{accountingMenu}
+
+ ) : null; + + const isEditAccounting = isAccountEditing ? ( + + ) : ( +

{accountTitle}

+ ); + return ( <> {/* Info: (20240927 - Julian) Accounting */} -
-

Accounting

- +
+
+ {isEditAccounting} +
+ +
+
+ {/* Info: (20241001 - Julian) Accounting Menu */} + {displayedAccountingMenu}
{/* Info: (20240927 - Julian) Particulars */} {/* Info: (20240927 - Julian) Debit */} {/* Info: (20240927 - Julian) Credit */} {/* Info: (20240927 - Julian) Delete button */}
-
@@ -61,6 +214,17 @@ const VoucherLineItem = () => { }; const VoucherLineBlock = () => { + const [lineItems, setLineItems] = useState([ + // Info: (20241001 - Julian) 初始傳票列 + { + id: 0, + account: null, + particulars: '', + debit: 0, + credit: 0, + }, + ]); + // ToDo: (20240927 - Julian) Implement total calculation // eslint-disable-next-line @typescript-eslint/no-unused-vars const [totalCredit, setTotalCredit] = useState(0); @@ -70,7 +234,80 @@ const VoucherLineBlock = () => { const totalStyle = totalCredit === totalDebit ? 'text-text-state-success-invert' : 'text-text-state-error-invert'; - const voucherLines = Array.from({ length: 5 }, (_, index) => ); + const AddNewVoucherLine = () => { + // Info: (20241001 - Julian) 取得最後一筆的 ID + 1,如果沒有資料就設定為 0 + const newVoucherId = lineItems.length > 0 ? lineItems[lineItems.length - 1].id + 1 : 0; + setLineItems([ + ...lineItems, + { + id: newVoucherId, + account: null, + particulars: '', + debit: 0, + credit: 0, + }, + ]); + }; + + useEffect(() => { + const debitTotal = lineItems.reduce((acc, item) => acc + item.debit, 0); + const creditTotal = lineItems.reduce((acc, item) => acc + item.credit, 0); + + setTotalDebit(debitTotal); + setTotalCredit(creditTotal); + }, [lineItems]); + + const voucherLines = lineItems.map((lineItem) => { + // Info: (20241001 - Julian) 複製傳票列 + const duplicateLineItem = { ...lineItem }; + + // Info: (20241001 - Julian) 刪除傳票列 + const deleteVoucherLine = () => { + setLineItems(lineItems.filter((item) => item.id !== lineItem.id)); + }; + + // Info: (20241001 - Julian) 設定 Account title + const accountTitleHandler = (account: IAccount | null) => { + duplicateLineItem.account = account; + setLineItems( + lineItems.map((item) => (item.id === duplicateLineItem.id ? duplicateLineItem : item)) + ); + }; + + // Info: (20241001 - Julian) 設定 Particulars + const particularsChangeHandler = (particulars: string) => { + duplicateLineItem.particulars = particulars; + setLineItems( + lineItems.map((item) => (item.id === duplicateLineItem.id ? duplicateLineItem : item)) + ); + }; + + // Info: (20241001 - Julian) 設定 Debit + const debitChangeHandler = (debit: number) => { + duplicateLineItem.debit = debit; + setLineItems( + lineItems.map((item) => (item.id === duplicateLineItem.id ? duplicateLineItem : item)) + ); + }; + + // Info: (20241001 - Julian) 設定 Credit + const creditChangeHandler = (credit: number) => { + duplicateLineItem.credit = credit; + setLineItems( + lineItems.map((item) => (item.id === duplicateLineItem.id ? duplicateLineItem : item)) + ); + }; + return ( + + ); + }); return (
@@ -89,16 +326,16 @@ const VoucherLineBlock = () => { {/* Info: (20240927 - Julian) Total calculation */} {/* Info: (20240927 - Julian) Total Debit */}
-

{numberWithCommas(totalCredit)}

+

{numberWithCommas(totalDebit)}

{/* Info: (20240927 - Julian) Total Debit */}
-

{numberWithCommas(totalDebit)}

+

{numberWithCommas(totalCredit)}

{/* Info: (20240927 - Julian) Add button */}
-