Skip to content
Open
Show file tree
Hide file tree
Changes from 17 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
6 changes: 5 additions & 1 deletion mysql/seed/000001_initial_schema_seed.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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>(
selectedPaidByUserId ?? null,
);

useEffect(() => {
if (!isOpen) return;
setDraftBureauId(selectedBureauId);
setDraftPaidByUserId(selectedPaidByUserId ?? null);
}, [isOpen, selectedBureauId, 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 paidBySelectValue = draftPaidByUserId ?? '';

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

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

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={labelClassName}>局名</p>
<Select
className={selectTextClassName}
value={draftBureauId ?? ''}
onChange={handleBureauChange}
>
<option className={optionClassName} value=''>
絞り込みなし
</option>
{bureaus.map((bureau) => (
<option className={optionClassName} key={bureau.id ?? 0} value={bureau.id ?? 0}>
{bureau.name}
</option>
))}
</Select>
</div>
<div>
<p className={labelClassName}>氏名</p>
<Select
className={selectTextClassName}
value={paidBySelectValue}
onChange={handlePaidByChange}
>
<option className={optionClassName} value=''>
絞り込みなし
</option>
{filteredUsers.map((user) => {
const bureauName = bureauNameMap.get(user.bureauID);
const label = draftBureauId || !bureauName ? user.name : `${bureauName} ${user.name}`;
return (
<option className={optionClassName} 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,31 @@
interface PurchaseReportSummaryAmountsProps {
unsettledAmountText: string;
unpackedAmountText: string;
className?: string;
}

export default function PurchaseReportSummaryAmounts({
unsettledAmountText,
unpackedAmountText,
className = '',
}: PurchaseReportSummaryAmountsProps) {
return (
<div className={className}>
<div className='inline-grid grid-cols-[auto_auto_auto] gap-1 text-sm font-medium leading-normal text-[#666] [font-family:"Noto_Sans_JP"]'>
<span className='whitespace-nowrap'>未清算金額</span>
<span className='whitespace-nowrap'>:</span>
<span className='min-w-[12ch] whitespace-nowrap text-right'>
{unsettledAmountText}
{'\u00A0\u00A0'}円
</span>

<span className='whitespace-nowrap'>未封詰め金額</span>
<span className='whitespace-nowrap'>:</span>
<span className='min-w-[12ch] whitespace-nowrap text-right'>
{unpackedAmountText}
{'\u00A0\u00A0'}円
</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';
108 changes: 102 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,71 @@ 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<User>;
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<number>(0);

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 +158,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 +244,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 +285,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 +309,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 +332,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 +343,7 @@ export default function PurchaseReports() {
}}
/>
</td>

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

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