Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
eda29a3
[add] Add buy reports summary endpoint and related types
nakatashingo Dec 28, 2025
3fa14ff
[add] Implement PurchaseReportSummaryAmounts component and integrate …
nakatashingo Dec 28, 2025
9d47aeb
[otr] Simplify JSX formatting in PurchaseReportSummaryAmounts component
nakatashingo Dec 28, 2025
44167c1
[add] PurchaseReportPaidByFilterModal component and integrate it into…
nakatashingo Dec 31, 2025
3b6ec0a
[fix] Update button text in PurchaseReportPaidByFilterModal
nakatashingo Dec 31, 2025
3c61495
[fix] Update button text in PurchaseReportPaidByFilterModal to improv…
nakatashingo Dec 31, 2025
dff5edd
[add] Implement PurchaseReportSummaryAmounts component and integrate …
nakatashingo Dec 28, 2025
72f01f1
[otr] Simplify JSX formatting in PurchaseReportSummaryAmounts component
nakatashingo Dec 28, 2025
95d6546
[add] PurchaseReportPaidByFilterModal component and integrate it into…
nakatashingo Dec 31, 2025
2c7c7b7
[fix] Update button text in PurchaseReportPaidByFilterModal
nakatashingo Dec 31, 2025
2894b01
[fix] Update button text in PurchaseReportPaidByFilterModal to improv…
nakatashingo Dec 31, 2025
d88ee44
[add] Add "絞り込みなし" option to bureau selection in PurchaseReportPaidBy…
nakatashingo Jan 24, 2026
96fdbdb
refactor: Reorganize imports and clean up whitespace in PurchaseRepor…
nakatashingo Jan 24, 2026
29d8823
formatted by workflow
nakatashingo Jan 24, 2026
ffe2764
[add] Insert additional payment receipt entries in initial schema seed
nakatashingo Jan 30, 2026
1b78882
[fix] Improve styling and structure in PurchaseReportPaidByFilterModa…
nakatashingo Jan 30, 2026
b7cda1c
[fix] Simplify user filtering logic and improve type checking in Purc…
nakatashingo Jan 31, 2026
9dbe13b
[fix] Update PurchaseReportPaidByFilterModal to use paidBy instead of…
nakatashingo Feb 4, 2026
566c3a9
[fix] Enhance PurchaseReportPaidByFilterModal and PurchaseReports to …
nakatashingo Feb 12, 2026
bf65e4f
[add] Implement search functionality and dropdown for paidBy selectio…
nakatashingo May 12, 2026
11ed226
[fix] Refactor filtering logic in PurchaseReports for improved readab…
nakatashingo May 12, 2026
d632646
[add] Create styles for name selection in PurchaseReportPaidByFilterM…
nakatashingo May 12, 2026
a160d56
[fix] Correct CSS background formatting and re-import styles in Purch…
nakatashingo May 12, 2026
38d8d19
[refactor] Remove CSS styles and integrate react-select for name sele…
nakatashingo May 12, 2026
db78d6f
[refactor] Clean up formatting and improve readability in PurchaseRep…
nakatashingo May 12, 2026
0101f1f
Merge branch 'develop' into feat/walt/purchase-report-sort-frontend
nakatashingo May 12, 2026
906cf6a
[refactor] Replace userAtom with useCurrentUser hook in PurchaseRepor…
nakatashingo May 12, 2026
b8d26b8
[fix] Ensure buy report data is updated after status change in Purcha…
nakatashingo May 12, 2026
1c1db0a
[refactor] Improve formatting and readability in PurchaseReportPaidBy…
nakatashingo May 12, 2026
d8d2705
[refactor] Add userNameMap to map user IDs to names in PurchaseReport…
nakatashingo May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import React, { FC, useEffect, useMemo, useState } from 'react';

import { CloseButton, Modal, OutlinePrimaryButton, Select } from '@components/common';
import { Bureau, User } from '@type/common';

interface PurchaseReportPaidByFilterModalProps {
isOpen: boolean;
onClose: () => void;
onApply: (selection: {
bureauId: number | null;
paidByUserId: number | null | undefined;
}) => void;
bureaus: Bureau[];
users: User[];
selectedBureauId: number | null;
selectedPaidByUserId: number | null | undefined;
}

const PurchaseReportPaidByFilterModal: FC<PurchaseReportPaidByFilterModalProps> = (props) => {
const { isOpen, onClose, onApply, bureaus, users, selectedBureauId, selectedPaidByUserId } =
props;

const [draftBureauId, setDraftBureauId] = useState<number | null>(selectedBureauId);
const [draftPaidByUserId, setDraftPaidByUserId] = useState<number | null | undefined>(
selectedPaidByUserId,
);

useEffect(() => {
if (!isOpen) return;
setDraftBureauId(selectedBureauId);
setDraftPaidByUserId(selectedPaidByUserId);
}, [isOpen, selectedBureauId, selectedPaidByUserId]);

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 paidBySelectValue = draftPaidByUserId === null ? 'none' : draftPaidByUserId ?? '';

const handleBureauChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const value = event.target.value;
const nextBureauId = value === '' ? null : Number(value);
setDraftBureauId(nextBureauId);
setDraftPaidByUserId(undefined);
};

const handlePaidByChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const value = event.target.value;
if (value === '') {
setDraftPaidByUserId(undefined);
return;
}
if (value === 'none') {
setDraftPaidByUserId(null);
return;
}
setDraftPaidByUserId(Number(value));
};
Comment thread
nakatashingo marked this conversation as resolved.
Outdated

const handleApply = () => {
onApply({
bureauId: draftBureauId ?? null,
paidByUserId: draftPaidByUserId,
});
};

return (
<Modal className='w-[90vw] max-w-[440px] p-6 shadow-lg' onClick={onClose}>
<div className='flex justify-end'>
<CloseButton onClick={onClose} />
</div>
<div className='mt-2 space-y-5'>
<div>
<p className='mb-2 text-sm text-black-600'>局名</p>
<Select value={draftBureauId ?? ''} onChange={handleBureauChange}>
<option value=''>絞り込みなし</option>
{bureaus.map((bureau) => (
<option key={bureau.id ?? 0} value={bureau.id ?? 0}>
{bureau.name}
</option>
))}
</Select>
</div>
<div>
<p className='mb-2 text-sm text-black-600'>氏名</p>
<Select value={paidBySelectValue} onChange={handlePaidByChange}>
<option value='none'>絞り込みなし</option>
{filteredUsers.map((user) => {
const bureauName = bureauNameMap.get(user.bureauID);
const label = draftBureauId || !bureauName ? user.name : `${bureauName} ${user.name}`;
return (
<option key={user.id} value={user.id}>
{label}
</option>
);
})}
</Select>
{/* NOTE: paid_by_user_id の NULL を絞り込む仕様が未定義のため「立替者なし」は未実装。 */}
</div>
</div>
<div className='mt-6 flex justify-center'>
<OutlinePrimaryButton onClick={handleApply}>絞り込む</OutlinePrimaryButton>
</div>
</Modal>
);
};

export default PurchaseReportPaidByFilterModal;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';

interface PurchaseReportSummaryAmountsProps {
unsettledAmountText: string;
unpackedAmountText: string;
className?: string;
}

export default function PurchaseReportSummaryAmounts({
unsettledAmountText,
unpackedAmountText,
className = 'text-sm text-black-600 md:ml-auto',
}: PurchaseReportSummaryAmountsProps) {
return (
<div className={className}>
<div className='inline-grid grid-cols-[auto_auto_auto] gap-1'>
<span className='whitespace-nowrap'>未清算金額</span>
<span className='whitespace-nowrap'>:</span>
<span className='min-w-[12ch] whitespace-nowrap text-right'>{unsettledAmountText} 円</span>
<span className='whitespace-nowrap'>未封詰め金額</span>
<span className='whitespace-nowrap'>:</span>
<span className='min-w-[12ch] whitespace-nowrap text-right'>{unpackedAmountText} 円</span>
</div>
</div>
);
}
1 change: 1 addition & 0 deletions view/next-project/src/components/purchasereports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
101 changes: 95 additions & 6 deletions view/next-project/src/pages/purchase_report_list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
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';
Expand All @@ -18,10 +24,12 @@ 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();
Expand All @@ -31,30 +39,64 @@ export default function PurchaseReports() {
error: yearPeriodsError,
} = useGetYearsPeriods();
const yearPeriods = yearPeriodsData?.data;

const user = useRecoilValue(userAtom);

const { data: usersResponse } = useGetUsers();
const users = useMemo(() => {
const responseData = usersResponse?.data as User[] | { data?: User[] } | undefined;
if (Array.isArray(responseData)) return responseData;
return responseData?.data ?? [];
Comment thread
nakatashingo marked this conversation as resolved.
Outdated
}, [usersResponse]);

user?.roleID === 1 && router.push('/my_page');

const [selectedYear, setSelectedYear] = useState<number>(
yearPeriods && yearPeriods.length > 0 ? yearPeriods[yearPeriods.length - 1].year : 0,
Comment thread
nakatashingo marked this conversation as resolved.
Outdated
);

useEffect(() => {
if (yearPeriods && yearPeriods.length > 0) {
const latestYear = Math.max(...yearPeriods.map((period) => period.year));
setSelectedYear(latestYear);
}
}, [yearPeriods]);

const [selectedYear, setSelectedYear] = useState<number>(
yearPeriods && yearPeriods.length > 0 ? yearPeriods[yearPeriods.length - 1].year : 0,
const [isPaidByFilterOpen, setIsPaidByFilterOpen] = useState(false);
const [selectedBureauId, setSelectedBureauId] = useState<number | null>(null);
const [selectedPaidByUserId, setSelectedPaidByUserId] = useState<number | null | undefined>(
undefined,
);
const getBuyReportsDetailsParams: GetBuyReportsDetailsParams = { year: selectedYear };

const getBuyReportsDetailsParams: GetBuyReportsDetailsParams = {
year: selectedYear,
...(selectedBureauId != null ? { financial_record_id: selectedBureauId } : {}),
...(selectedPaidByUserId != null ? { paid_by_user_id: selectedPaidByUserId } : {}),
};

const {
data: buyReportsData,
isLoading: isBuyReportsLoading,
error: buyReportsError,
mutate: mutateBuyReportData,
} = useGetBuyReportsDetails(getBuyReportsDetailsParams);

const buyReports = useMemo(() => buyReportsData?.data ?? [], [buyReportsData]);

const getBuyReportsSummaryParams: GetBuyReportsSummaryParams = {
year: selectedYear,
...(selectedBureauId != null ? { financial_record_id: selectedBureauId } : {}),
...(selectedPaidByUserId != null ? { paid_by_user_id: selectedPaidByUserId } : {}),
};

const {
data: buyReportsSummaryData,
isLoading: isBuyReportsSummaryLoading,
error: buyReportsSummaryError,
} = useGetBuyReportsSummary(getBuyReportsSummaryParams, {
swr: { enabled: selectedYear > 0 },
});

const [sealChecks, setSealChecks] = useState<Record<number, boolean>>({});
const [settlementChecks, setSettlementChecks] = useState<Record<number, boolean>>({});

Expand Down Expand Up @@ -109,6 +151,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);
Expand Down Expand Up @@ -183,8 +237,29 @@ export default function PurchaseReports() {
CSVダウンロード
<TbDownload className='ml-2' size={20} />
</PrimaryButton>
<PurchaseReportSummaryAmounts
unsettledAmountText={summaryUnsettledAmount}
unpackedAmountText={summaryUnpackedAmount}
/>
</div>
</div>

{isPaidByFilterOpen && (
<PurchaseReportPaidByFilterModal
isOpen={isPaidByFilterOpen}
Comment thread
nakatashingo marked this conversation as resolved.
Outdated
onClose={() => setIsPaidByFilterOpen(false)}
onApply={({ bureauId, paidByUserId }) => {
setSelectedBureauId(bureauId);
setSelectedPaidByUserId(paidByUserId);
setIsPaidByFilterOpen(false);
}}
bureaus={BUREAUS}
users={users}
selectedBureauId={selectedBureauId}
selectedPaidByUserId={selectedPaidByUserId}
/>
)}

<div className='mt-2 flex-1 overflow-auto p-4 md:p-8'>
<div className='min-w-max'>
<table className='mb-5 table-auto border-collapse'>
Expand All @@ -203,7 +278,17 @@ export default function PurchaseReports() {
物品
</th>
<th className='whitespace-nowrap px-4 pb-2 text-sm font-normal text-black-600'>
立替者
<div className='flex items-center justify-center gap-1'>
<span>立替者</span>
<button
type='button'
className='rounded-full p-0.5 text-black-600 hover:bg-white-100'
onClick={() => setIsPaidByFilterOpen(true)}
aria-label='立替者の絞り込み'
>
<RiArrowDropDownLine size={20} />
</button>
</div>
</th>
<th className='whitespace-nowrap px-4 pb-2 text-sm font-normal text-black-600'>
金額
Expand All @@ -217,6 +302,7 @@ export default function PurchaseReports() {
<th className='whitespace-nowrap px-4 pb-2 text-sm text-black-600'></th>
</tr>
</thead>

<tbody>
{buyReports && buyReports.length > 0 ? (
buyReports.map((report) => (
Expand All @@ -239,6 +325,7 @@ export default function PurchaseReports() {
<td className='whitespace-nowrap px-4 py-3 text-center text-sm text-black-600'>
{formatAmount(report.amount ?? 0)}
</td>

<td className='px-4 py-2 text-center'>
<Checkbox
className='accent-primary-5'
Expand All @@ -249,6 +336,7 @@ export default function PurchaseReports() {
}}
/>
</td>

<td className='px-4 py-2 text-center'>
<OpenCheckSettlementModalButton
id={report.id ?? 0}
Expand All @@ -260,6 +348,7 @@ export default function PurchaseReports() {
disabled={!sealChecks[report.id ?? 0]}
/>
</td>

<td>
<div className='flex'>
<div className='mx-1'>
Expand Down