diff --git a/mysql/seed/000001_initial_schema_seed.sql b/mysql/seed/000001_initial_schema_seed.sql index 1c094ecde..aef5854d9 100644 --- a/mysql/seed/000001_initial_schema_seed.sql +++ b/mysql/seed/000001_initial_schema_seed.sql @@ -78,7 +78,11 @@ INSERT INTO VALUES (1, 'payment-receipts', 'receipt-1.jpg', 'image/jpeg', ''), (2, 'payment-receipts', 'receipt-2.jpg', 'image/jpeg', ''), - (3, 'payment-receipts', 'receipt-3.jpg', 'image/jpeg', ''); + (3, 'payment-receipts', 'receipt-3.jpg', 'image/jpeg', ''), + (4, 'payment-receipts', 'receipt-4.jpg', 'image/jpeg', ''), + (5, 'payment-receipts', 'receipt-5.jpg', 'image/jpeg', ''), + (6, 'payment-receipts', 'receipt-6.jpg', 'image/jpeg', ''), + (7, 'payment-receipts', 'receipt-7.jpg', 'image/jpeg', ''); INSERT INTO buy_statuses (buy_report_id, is_packed, is_settled) diff --git a/view/next-project/src/components/purchasereports/PurchaseReportPaidByFilterModal.tsx b/view/next-project/src/components/purchasereports/PurchaseReportPaidByFilterModal.tsx new file mode 100644 index 000000000..5367ec749 --- /dev/null +++ b/view/next-project/src/components/purchasereports/PurchaseReportPaidByFilterModal.tsx @@ -0,0 +1,168 @@ +import React, { FC, useEffect, useMemo, useState } from 'react'; + +import { CloseButton, Modal, OutlinePrimaryButton, Select } from '@components/common'; +import { Bureau, User } from '@type/common'; +import { normalizePaidBy } from '@/utils/purchaseReportFilters'; + +interface PurchaseReportPaidByFilterModalProps { + isOpen: boolean; + onClose: () => void; + onApply: (selection: { + bureauId: number | null; + paidByUserId: number | null; + paidBy: string | null | undefined; + }) => void; + bureaus: Bureau[]; + users: User[]; + selectedBureauId: number | null; + selectedPaidByUserId: number | null; + selectedPaidBy: string | null | undefined; +} + +const PurchaseReportPaidByFilterModal: FC = (props) => { + const { + isOpen, + onClose, + onApply, + bureaus, + users, + selectedBureauId, + selectedPaidByUserId, + selectedPaidBy, + } = props; + + const [draftBureauId, setDraftBureauId] = useState(selectedBureauId); + const [draftPaidByUserId, setDraftPaidByUserId] = useState( + selectedPaidByUserId ?? null, + ); + const [draftPaidBy, setDraftPaidBy] = useState(normalizePaidBy(selectedPaidBy)); + + useEffect(() => { + if (!isOpen) return; + setDraftBureauId(selectedBureauId); + setDraftPaidByUserId(selectedPaidByUserId ?? null); + setDraftPaidBy(normalizePaidBy(selectedPaidBy)); + }, [isOpen, selectedBureauId, selectedPaidBy, selectedPaidByUserId]); + + const labelClassName = 'mb-2 text-sm text-black-600 [font-family:"Noto_Sans_JP"]'; + const selectTextClassName = 'text-black-600 [font-family:"Noto_Sans_JP"]'; + const optionClassName = 'text-black-600 [font-family:"Noto_Sans_JP"]'; + + const bureauNameMap = useMemo( + () => + new Map( + bureaus.map((bureau) => [bureau.id ?? 0, bureau.name] as const).filter(([id]) => id > 0), + ), + [bureaus], + ); + + const filteredUsers = useMemo(() => { + if (!draftBureauId) return users; + return users.filter((user) => user.bureauID === draftBureauId); + }, [draftBureauId, users]); + + const legacyPaidBy = draftPaidByUserId == null ? draftPaidBy : null; + const legacyPaidByValue = legacyPaidBy ? `legacy:${legacyPaidBy}` : ''; + const paidBySelectValue = + draftPaidByUserId != null ? String(draftPaidByUserId) : legacyPaidByValue; + + const handleBureauChange = (event: React.ChangeEvent) => { + const value = event.target.value; + const nextBureauId = value === '' ? null : Number(value); + setDraftBureauId(nextBureauId); + setDraftPaidByUserId(null); + setDraftPaidBy(null); + }; + + const handlePaidByChange = (event: React.ChangeEvent) => { + const value = event.target.value; + if (value === '') { + setDraftPaidByUserId(null); + setDraftPaidBy(null); + return; + } + + if (value.startsWith('legacy:')) { + setDraftPaidByUserId(null); + setDraftPaidBy(normalizePaidBy(value.replace('legacy:', ''))); + return; + } + + const nextUserId = Number(value); + if (!Number.isFinite(nextUserId) || nextUserId <= 0) { + setDraftPaidByUserId(null); + setDraftPaidBy(null); + return; + } + + const selectedUser = users.find((user) => user.id === nextUserId); + setDraftPaidByUserId(nextUserId); + setDraftPaidBy(normalizePaidBy(selectedUser?.name ?? null)); + }; + + const handleApply = () => { + onApply({ + bureauId: draftBureauId ?? null, + paidByUserId: draftPaidByUserId ?? null, + paidBy: normalizePaidBy(draftPaidBy), + }); + }; + + return ( + +
+ +
+
+
+

局名

+ +
+
+

氏名

+ +
+
+
+ 絞り込む +
+
+ ); +}; + +export default PurchaseReportPaidByFilterModal; diff --git a/view/next-project/src/components/purchasereports/PurchaseReportSummaryAmounts.tsx b/view/next-project/src/components/purchasereports/PurchaseReportSummaryAmounts.tsx new file mode 100644 index 000000000..c807d76ff --- /dev/null +++ b/view/next-project/src/components/purchasereports/PurchaseReportSummaryAmounts.tsx @@ -0,0 +1,31 @@ +interface PurchaseReportSummaryAmountsProps { + unsettledAmountText: string; + unpackedAmountText: string; + className?: string; +} + +export default function PurchaseReportSummaryAmounts({ + unsettledAmountText, + unpackedAmountText, + className = '', +}: PurchaseReportSummaryAmountsProps) { + return ( +
+
+ 未清算金額 + + + {unsettledAmountText} + {'\u00A0\u00A0'}円 + + + 未封詰め金額 + + + {unpackedAmountText} + {'\u00A0\u00A0'}円 + +
+
+ ); +} diff --git a/view/next-project/src/components/purchasereports/index.ts b/view/next-project/src/components/purchasereports/index.ts index eca7bd61e..ecd14bde4 100644 --- a/view/next-project/src/components/purchasereports/index.ts +++ b/view/next-project/src/components/purchasereports/index.ts @@ -11,5 +11,6 @@ export { default as PurchaseOrderListModal } from './PurchaseOrderListModal'; export { default as PurchaseReportAddModal } from './PurchaseReportAddModal'; export { default as PurchaseReportConfirmModal } from './PurchaseReportConfirmModal'; export { default as PurchaseReportItemNumModal } from './PurchaseReportItemNumModal'; // "PurchaseReport|temNumModal"を修正しました。 +export { default as PurchaseReportSummaryAmounts } from './PurchaseReportSummaryAmounts'; export { default as ReceiptModal } from './ReceiptModal'; export { default as CheckSettlementConfirmModal } from './CheckSettlementConfirmModal'; diff --git a/view/next-project/src/pages/purchase_report_list/index.tsx b/view/next-project/src/pages/purchase_report_list/index.tsx index e583e3e1c..bf3426639 100644 --- a/view/next-project/src/pages/purchase_report_list/index.tsx +++ b/view/next-project/src/pages/purchase_report_list/index.tsx @@ -1,27 +1,36 @@ import { saveAs } from 'file-saver'; import { useRouter } from 'next/router'; -import { useCallback, useState, useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { RiArrowDropDownLine } from 'react-icons/ri'; import { TbDownload } from 'react-icons/tb'; import { useRecoilValue } from 'recoil'; import DownloadButton from '@/components/common/DownloadButton'; import PrimaryButton from '@/components/common/OutlinePrimaryButton/OutlinePrimaryButton'; import { OpenCheckSettlementModalButton } from '@/components/purchasereports'; +import PurchaseReportPaidByFilterModal from '@/components/purchasereports/PurchaseReportPaidByFilterModal'; +import PurchaseReportSummaryAmounts from '@/components/purchasereports/PurchaseReportSummaryAmounts'; +import { BUREAUS } from '@/constants/bureaus'; import { useGetBuyReportsDetails, + useGetBuyReportsSummary, + useGetUsers, useGetYearsPeriods, usePutBuyReportStatusBuyReportId, } from '@/generated/hooks'; import { userAtom } from '@/store/atoms'; +import { buildPaidByFilterParams } from '@/utils/purchaseReportFilters'; import { Card, Checkbox, EditButton, Loading, Title } from '@components/common'; import MainLayout from '@components/layout/MainLayout'; import OpenDeleteModalButton from '@components/purchasereports/OpenDeleteModalButton'; import type { - GetBuyReportsDetailsParams, BuyReportDetail, + GetBuyReportsDetailsParams, + GetBuyReportsSummaryParams, PutBuyReportStatusBuyReportIdBody, } from '@/generated/model'; +import type { User } from '@type/common'; export default function PurchaseReports() { const router = useRouter(); @@ -31,10 +40,29 @@ export default function PurchaseReports() { error: yearPeriodsError, } = useGetYearsPeriods(); const yearPeriods = yearPeriodsData?.data; + const user = useRecoilValue(userAtom); + const { data: usersResponse } = useGetUsers(); + const isUser = (value: unknown): value is User => { + if (!value || typeof value !== 'object') return false; + const candidate = value as Partial; + return typeof candidate.id === 'number' && typeof candidate.name === 'string'; + }; + const users = useMemo(() => { + const responseData: unknown = usersResponse?.data; + if (Array.isArray(responseData)) return responseData.filter(isUser); + if (responseData && typeof responseData === 'object') { + const nested = (responseData as { data?: unknown }).data; + if (Array.isArray(nested)) return nested.filter(isUser); + } + return []; + }, [usersResponse]); + user?.roleID === 1 && router.push('/my_page'); + const [selectedYear, setSelectedYear] = useState(0); + useEffect(() => { if (yearPeriods && yearPeriods.length > 0) { const latestYear = Math.max(...yearPeriods.map((period) => period.year)); @@ -42,10 +70,21 @@ export default function PurchaseReports() { } }, [yearPeriods]); - const [selectedYear, setSelectedYear] = useState( - yearPeriods && yearPeriods.length > 0 ? yearPeriods[yearPeriods.length - 1].year : 0, - ); - const getBuyReportsDetailsParams: GetBuyReportsDetailsParams = { year: selectedYear }; + const [isPaidByFilterOpen, setIsPaidByFilterOpen] = useState(false); + const [selectedBureauId, setSelectedBureauId] = useState(null); + const [selectedPaidBy, setSelectedPaidBy] = useState(undefined); + const [selectedPaidByUserId, setSelectedPaidByUserId] = useState(null); + + const paidByFilterParams = buildPaidByFilterParams({ + paidByUserId: selectedPaidByUserId, + paidBy: selectedPaidBy, + }); + + const getBuyReportsDetailsParams: GetBuyReportsDetailsParams = { + year: selectedYear, + ...(selectedBureauId != null ? { financial_record_id: selectedBureauId } : {}), + ...paidByFilterParams, + }; const { data: buyReportsData, @@ -53,8 +92,23 @@ export default function PurchaseReports() { error: buyReportsError, mutate: mutateBuyReportData, } = useGetBuyReportsDetails(getBuyReportsDetailsParams); + const buyReports = useMemo(() => buyReportsData?.data ?? [], [buyReportsData]); + const getBuyReportsSummaryParams: GetBuyReportsSummaryParams = { + year: selectedYear, + ...(selectedBureauId != null ? { financial_record_id: selectedBureauId } : {}), + ...paidByFilterParams, + }; + + const { + data: buyReportsSummaryData, + isLoading: isBuyReportsSummaryLoading, + error: buyReportsSummaryError, + } = useGetBuyReportsSummary(getBuyReportsSummaryParams, { + swr: { enabled: selectedYear > 0 }, + }); + const [sealChecks, setSealChecks] = useState>({}); const [settlementChecks, setSettlementChecks] = useState>({}); @@ -109,6 +163,18 @@ export default function PurchaseReports() { return amount.toLocaleString(); }, []); + const buyReportsSummary = buyReportsSummaryData?.data; + + const summaryUnsettledAmount = + isBuyReportsSummaryLoading || buyReportsSummaryError || buyReportsSummary == null + ? '-' + : formatAmount(buyReportsSummary.unsettledAmount ?? 0); + + const summaryUnpackedAmount = + isBuyReportsSummaryLoading || buyReportsSummaryError || buyReportsSummary == null + ? '-' + : formatAmount(buyReportsSummary.unpackedAmount ?? 0); + const download = async (url: string, fileName: string) => { const downloadPath = `${process.env.NEXT_PUBLIC_MINIO_ENDPONT}/finansu/${url}`; const response = await fetch(downloadPath); @@ -183,8 +249,31 @@ export default function PurchaseReports() { CSVダウンロード + + + {isPaidByFilterOpen && ( + setIsPaidByFilterOpen(false)} + onApply={({ bureauId, paidByUserId, paidBy }) => { + setSelectedBureauId(bureauId); + setSelectedPaidByUserId(paidByUserId ?? null); + setSelectedPaidBy(paidBy); + setIsPaidByFilterOpen(false); + }} + bureaus={BUREAUS} + users={users} + selectedBureauId={selectedBureauId} + selectedPaidByUserId={selectedPaidByUserId} + selectedPaidBy={selectedPaidBy} + /> + )} +
@@ -203,7 +292,17 @@ export default function PurchaseReports() { 物品 + {buyReports && buyReports.length > 0 ? ( buyReports.map((report) => ( @@ -239,6 +339,7 @@ export default function PurchaseReports() { + + +
- 立替者 +
+ 立替者 + +
金額 @@ -217,6 +316,7 @@ export default function PurchaseReports() {
{formatAmount(report.amount ?? 0)}
diff --git a/view/next-project/src/utils/purchaseReportFilters.ts b/view/next-project/src/utils/purchaseReportFilters.ts new file mode 100644 index 000000000..f17286dd3 --- /dev/null +++ b/view/next-project/src/utils/purchaseReportFilters.ts @@ -0,0 +1,34 @@ +export type PaidByFilterInput = { + paidByUserId?: number | null; + paidBy?: string | null | undefined; +}; + +export type PaidByFilterParams = { + paid_by_user_id?: number; + paid_by?: string; +}; + +export const normalizePaidBy = (value: string | null | undefined): string | null => { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +}; + +const normalizePaidByUserId = (value: number | null | undefined): number | null => { + if (typeof value !== 'number' || Number.isNaN(value) || value <= 0) return null; + return value; +}; + +export const buildPaidByFilterParams = (input: PaidByFilterInput): PaidByFilterParams => { + const paidByUserId = normalizePaidByUserId(input.paidByUserId); + if (paidByUserId != null) { + return { paid_by_user_id: paidByUserId }; + } + + const paidBy = normalizePaidBy(input.paidBy); + if (paidBy != null) { + return { paid_by: paidBy }; + } + + return {}; +};