/**
 * 決済管理ページ
 *
 * 決済一覧・入金消込・返金・売上集計・CSVエクスポート。
 */
import { useState, useEffect, useCallback } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import {
	Button,
	TextControl,
	TextareaControl,
	SelectControl,
	Modal,
	Notice,
} from '@wordpress/components';
import apiFetch from '@wordpress/api-fetch';
import DataTable from '../components/DataTable';
import downloadBase64Pdf from '../../common/pdf-download';
import useCan from '../hooks/useCan';
import GuideSection from '../components/GuideSection';
import { apiErrorMessage } from '../../common/api-error';

/**
 * 領収書ボタンを出す決済ステータス
 *
 * 返金済でも出すのは、**入金時に発行した控えを後から読めなくしないため**。
 * 新規発行は ReceiptGenerator::create() が completed に限っているので、
 * 控えが無い返金済は 400 で断られる（メッセージが理由を言う）。
 */
// ⚠️ サーバ側の正典は PaymentDocuments::RECEIPT_VISIBLE_STATUSES。M-18 が一致を見る。
const RECEIPT_STATUSES = [ 'completed', 'refunded', 'partial_refund' ];

/**
 * 請求書ボタンを出す決済ステータス
 *
 * 🔴 ここが無いあいだ、請求書ボタンは `canEdit` だけで出ていた＝**どの状態でも押せた**。
 * サーバ側 `PaymentDocuments::BILLABLE_STATUSES` と対。**片方だけ変えない**。
 *
 * ⚠️ 領収書と違って「控えの再取得」のための広げ方はしない。請求書は入金前に
 * 出すものなので、既に発行済みなら状態は `pending` 以降のこの一覧に必ず入っている。
 */
const BILLABLE_STATUSES = [ 'pending', 'completed', 'refunded', 'partial_refund' ];

const METHOD_LABELS = {
	bank: __( 'Bank Transfer', 'conferencemanager' ),
	stripe: 'Stripe',
};

const STATUS_LABELS = {
	pending: __( 'Unpaid', 'conferencemanager' ),
	completed: __( 'Paid', 'conferencemanager' ),
	refunded: __( 'Refunded', 'conferencemanager' ),
	partial_refund: __( 'Partial Refund', 'conferencemanager' ),
	failed: __( 'Failed', 'conferencemanager' ),
	cancelled: __( 'Withdrawn', 'conferencemanager' ),
};

const STATUS_COLORS = {
	pending: '#d63638',
	completed: '#00a32a',
	refunded: '#757575',
	partial_refund: '#dba617',
	failed: '#b32d2e',
	// 取り下げは「異常」ではないので赤にしない（未入金の赤・失敗の濃赤と区別する）
	cancelled: '#757575',
};

// 請求金額（税込）が ¥0 の決済は入金を待つ必要がないため「請求不要」として表示する
const BILLING_FREE_LABEL = __( 'No billing required', 'conferencemanager' );
const BILLING_FREE_COLOR = '#1e1e1e';

const yen = ( val ) => `¥${ Number( val || 0 ).toLocaleString() }`;

// 請求額（税込）
function billedAmount( payment ) {
	return Number( payment.amount || 0 ) + Number( payment.tax || 0 );
}

function isBillingFree( payment ) {
	return 0 === billedAmount( payment );
}

// 精算状態（サーバ側 PaymentManager::resolve_settlement() と対応）
const SETTLEMENT_LABELS = {
	refund_due: __( 'Refund due', 'conferencemanager' ),
	surcharge_due: __( 'Surcharge due', 'conferencemanager' ),
};

const SETTLEMENT_COLORS = {
	refund_due: '#d63638',
	surcharge_due: '#dba617',
};

/**
 * 差額の色を返す
 *
 * 🔴 **符号で直に色を決めない。** 差額 > 0 は「要返金」・< 0 は「要追加徴収」なので、
 * 符号で `diff < 0 ? 赤 : 黄` と書くとステータス列のラベルとちょうど逆になる。
 * 同じ数字が一覧・集計カード・入金確認モーダル・詳細で違う色に見えていた
 * （精算モーダルだけが最初から SETTLEMENT_COLORS を使っていた）。
 * 差額を出す場所は **4 箇所**。全部この関数を通す（静的検査 M-16 が 0 件を保つ）。
 */
function diffColor( diff ) {
	if ( ! diff ) {
		return 'inherit';
	}
	return SETTLEMENT_COLORS[ diff > 0 ? 'refund_due' : 'surcharge_due' ] || 'inherit';
}

const ADJUSTMENT_METHODS = [
	{ label: __( 'Cash at desk', 'conferencemanager' ), value: 'cash' },
	{ label: __( 'Bank Transfer', 'conferencemanager' ), value: 'bank' },
	{ label: 'Stripe', value: 'stripe' },
];

// 入金が発生済みとみなすステータス（一度入金された後に返金されたものを含む）
const RECEIVED_STATUSES = [ 'completed', 'refunded', 'partial_refund' ];

/**
 * 実入金額を返す。未入金（消込前）は null。
 *
 * paid_amount 導入より前に消込された決済は paid_amount を持たないため、
 * 「請求額どおり入金された」とみなして請求額を返す（集計側の
 * COALESCE(paid_amount, amount + tax) と同じ扱い）。
 */
/**
 * サイトのタイムゾーンではなくブラウザの今日を返す
 */
function todayLocal() {
	const d = new Date();
	const pad = ( n ) => String( n ).padStart( 2, '0' );
	return `${ d.getFullYear() }-${ pad( d.getMonth() + 1 ) }-${ pad( d.getDate() ) }`;
}

function paidAmount( payment ) {
	if ( null !== payment.paid_amount && undefined !== payment.paid_amount && '' !== payment.paid_amount ) {
		return Number( payment.paid_amount );
	}
	return RECEIVED_STATUSES.includes( payment.status ) ? billedAmount( payment ) : null;
}

// 純入金額（入金額 − 返金 + 追加徴収）。サーバが算出した net_paid を優先する
function netPaid( payment ) {
	if ( null !== payment.net_paid && undefined !== payment.net_paid && '' !== payment.net_paid ) {
		return Number( payment.net_paid );
	}
	return paidAmount( payment );
}

// 請求額と純入金額の差額。判定できない場合は null（0 は「差額なし」を意味するため区別する）
function paidDiff( payment ) {
	const paid = netPaid( payment );
	return null === paid ? null : paid - billedAmount( payment );
}

// 精算が必要か（要返金／要追加徴収）
function settlementState( payment ) {
	if ( payment.settlement_state ) {
		return payment.settlement_state;
	}
	// 全額返金は取引の取り消しなので精算対象にしない（サーバ側 resolve_settlement と同じ規則）
	if ( 'refunded' === payment.status ) {
		return 'settled';
	}
	const diff = paidDiff( payment );
	if ( null === diff ) return 'unpaid';
	if ( diff > 0 ) return 'refund_due';
	if ( diff < 0 ) return 'surcharge_due';
	return 'settled';
}

/**
 * 送信できる精算の目的
 *
 * 🔴 ここに無い値では handleSettle() は**何もしない**。送信部の
 * `type: 'surcharge' === purpose ? 'surcharge' : 'refund'` は **else が 'refund'** なので、
 * 目的が空のまま到達すると「返金」として台帳に載ってしまう。
 * （delete_adjustment は明細ごとの取り消しで、この経路では送らない。）
 */
const SETTLE_PURPOSES = [ 'refund_diff', 'surcharge', 'refund_cancel', 'correct_paid' ];

/**
 * 目的ごとの既定額を返す
 *
 * ⚠️ **目的が変わったら必ず入れ直す。** 入力欄は 1 本で意味だけが変わるため、
 * 前の目的の数字が残ると「差額 ¥1,000」を「正しい入金額 ¥1,000」として送ってしまい、
 * 入金額の記録が壊れる（元は 4 択のうち 2 つでしか書き換えていなかった）。
 * correct_paid の既定は**受領額（paid_amount の生値）**。純入金額を使うと返金分を二重に引く。
 */
function defaultSettleAmount( payment, purpose ) {
	if ( ! payment ) {
		return '';
	}
	switch ( purpose ) {
		case 'refund_diff':
		case 'surcharge':
			return String( Math.abs( paidDiff( payment ) ?? 0 ) );
		case 'refund_cancel':
			return String( netPaid( payment ) ?? 0 );
		case 'correct_paid':
			return String( paidAmount( payment ) ?? 0 );
		default:
			return '';
	}
}

/**
 * 表示用ステータスを解決する。
 *
 * 入金不足・過入金は DB のステータス値ではなく、請求額と入金額の差額から導出する表示専用の状態。
 */
function resolveStatusDisplay( payment ) {
	if ( isBillingFree( payment ) ) {
		return { label: BILLING_FREE_LABEL, color: BILLING_FREE_COLOR };
	}

	if ( 'completed' === payment.status ) {
		const state = settlementState( payment );
		// 対応が必要な状態を最優先で示す
		if ( 'refund_due' === state || 'surcharge_due' === state ) {
			return { label: SETTLEMENT_LABELS[ state ], color: SETTLEMENT_COLORS[ state ] };
		}
	}

	return {
		label: STATUS_LABELS[ payment.status ] || payment.status,
		color: STATUS_COLORS[ payment.status ] || 'inherit',
	};
}

export default function Payments() {
	const [ items, setItems ] = useState( [] );
	const [ total, setTotal ] = useState( 0 );
	const [ page, setPage ] = useState( 1 );
	const [ statusFilter, setStatusFilter ] = useState( '' );
	const [ methodFilter, setMethodFilter ] = useState( '' );
	const [ eventFilter, setEventFilter ] = useState( '' );
	const [ loading, setLoading ] = useState( false );
	const [ notice, setNotice ] = useState( null );
	// 押している間は同じ行の 2 つのボタンを止める（連打で採番が走らないように）
	const [ issuingId, setIssuingId ] = useState( null );

	// イベント一覧
	const [ events, setEvents ] = useState( [] );

	// 売上集計
	const [ summary, setSummary ] = useState( null );

	// 入金確認モーダル
	const [ confirmModal, setConfirmModal ] = useState( null );
	const [ confirmName, setConfirmName ] = useState( '' );
	const [ confirmDate, setConfirmDate ] = useState( '' );
	const [ confirmPaidAmount, setConfirmPaidAmount ] = useState( '' );
	const [ confirmNote, setConfirmNote ] = useState( '' );
	// 入金日（受け取り側の通帳に入った日）。振込日とは別物。
	const [ confirmPaidAt, setConfirmPaidAt ] = useState( '' );

	// 詳細モーダル
	const [ detailModal, setDetailModal ] = useState( null );

	// 精算モーダル
	const [ settleModal, setSettleModal ] = useState( null );
	// 🔴 目的は**未選択**で開く。既定で「差額を返金する」が入っていると、
	//    窓口で 1 円も動いていなくてもボタンを押すだけで返金が台帳に載る。
	const [ settleForm, setSettleForm ] = useState( { purpose: '', amount: '', method: 'cash', occurred_at: '', reason: '' } );
	const [ adjustments, setAdjustments ] = useState( [] );
	const [ settling, setSettling ] = useState( false );
	const [ deletingId, setDeletingId ] = useState( null );

	// イベント一覧取得
	useEffect( () => {
		apiFetch( { path: '/conf-manager/v1/events?per_page=100' } )
			.then( ( data ) => {
				const list = Array.isArray( data ) ? data : data.items || [];
				setEvents( list );
			} )
			.catch( () => {} );
	}, [] );

	// 売上集計取得
	useEffect( () => {
		if ( ! eventFilter ) {
			setSummary( null );
			return;
		}
		apiFetch( { path: `/conf-manager/v1/payments/summary/${ eventFilter }` } )
			.then( setSummary )
			.catch( () => setSummary( null ) );
	}, [ eventFilter ] );

	const fetchItems = useCallback( async () => {
		setLoading( true );
		try {
			const params = new URLSearchParams( { page, per_page: 20 } );
			if ( statusFilter ) params.set( 'status', statusFilter );
			if ( methodFilter ) params.set( 'method', methodFilter );
			if ( eventFilter ) params.set( 'event_id', eventFilter );

			const response = await apiFetch( {
				path: `/conf-manager/v1/payments?${ params }`,
				parse: false,
			} );
			const data = await response.json();
			setItems( data );
			setTotal( parseInt( response.headers.get( 'X-WP-Total' ) || '0' ) );
		} catch ( err ) {
			// ⚠️ parse:false の失敗は Response がそのまま throw されるので err.message は undefined。
			setNotice( {
				status: 'error',
				message: await apiErrorMessage( err, __( 'Failed to load the payment list.', 'conferencemanager' ) ),
			} );
		} finally {
			setLoading( false );
		}
	}, [ page, statusFilter, methodFilter, eventFilter ] );

	useEffect( () => {
		fetchItems();
	}, [ fetchItems ] );

	const openConfirm = ( payment ) => {
		setConfirmModal( payment );
		setConfirmName( payment.bank_transfer_name || '' );
		setConfirmDate( '' );
		// 既定は請求額どおりの入金。過不足があった場合のみ管理者が書き換える。
		setConfirmPaidAmount( String( billedAmount( payment ) ) );
		setConfirmNote( payment.confirm_note || '' );
		// 既定は今日。既に入金日があればそれを出す（paid_at は GMT 保存なので現地日付側を使う）。
		setConfirmPaidAt( ( payment.paid_at_local || '' ).substring( 0, 10 ) || todayLocal() );
	};

	const handleConfirm = async () => {
		const paid = parseInt( confirmPaidAmount, 10 );
		if ( isNaN( paid ) || paid < 0 ) {
			setNotice( { status: 'error', message: __( 'Enter the paid amount as a number of 0 or more.', 'conferencemanager' ) } );
			return;
		}
		try {
			await apiFetch( {
				path: `/conf-manager/v1/payments/${ confirmModal.id }/confirm`,
				method: 'POST',
				data: {
					bank_transfer_name: confirmName,
					bank_transfer_date: confirmDate,
					paid_at: confirmPaidAt,
					paid_amount: paid,
					confirm_note: confirmNote,
				},
			} );
			setNotice( { status: 'success', message: __( 'Payment confirmed.', 'conferencemanager' ) } );
			setConfirmModal( null );
			fetchItems();
		} catch ( err ) {
			setNotice( { status: 'error', message: err.message } );
		}
	};

	// 精算（差額返金・追加徴収・全額返金＋参加取消・入金額の訂正・記録の取り消し）
	const openSettle = async ( payment ) => {
		setSettleModal( payment );
		// 🔴 **目的も金額も空で開く。** 差額をプリフィルしたうえで「差額を返金する」を
		//    選んだ状態にしておくと、開いて押すだけで実在しない返金が記録できる
		//    （実際に、返金 ¥1,000 が 4 件積まれて「要追加徴収 −¥4,000」になった）。
		setSettleForm( { purpose: '', amount: '', method: 'cash', occurred_at: '', reason: '' } );
		setAdjustments( [] );
		try {
			const data = await apiFetch( { path: `/conf-manager/v1/payments/${ payment.id }/adjustments` } );
			setAdjustments( data.items || [] );
		} catch ( err ) {
			setNotice( { status: 'error', message: err.message } );
		}
	};

	// 決済行に最新の精算結果を反映する（モーダルを開いたまま数字を更新する）
	const applySettlement = ( settlement ) => {
		if ( ! settlement ) return;
		setSettleModal( ( p ) => ( p ? {
			...p,
			refund_total: settlement.refund_total,
			surcharge_total: settlement.surcharge_total,
			net_paid: settlement.net_paid,
			settlement_diff: settlement.diff,
			settlement_state: settlement.state,
		} : p ) );
	};

	// 誤って記録した精算明細の取り消し（削除ではなく「無かったことにする」）
	const handleDeleteAdjustment = async ( adjustment ) => {
		if ( ! settleModal ) return;
		const warning = sprintf(
			/* translators: 1: settlement type label, 2: amount */
			__( 'Cancel this settlement record? %1$s %2$s will be removed from the history and from the totals. This is for records created by mistake — it does not undo an actual refund.', 'conferencemanager' ),
			'refund' === adjustment.type ? __( 'Refund', 'conferencemanager' ) : __( 'Surcharge', 'conferencemanager' ),
			yen( adjustment.amount )
		);
		if ( ! window.confirm( warning ) ) {
			return;
		}
		setDeletingId( adjustment.id );
		try {
			const data = await apiFetch( {
				path: `/conf-manager/v1/payments/${ settleModal.id }/adjustments/${ adjustment.id }`,
				method: 'DELETE',
			} );
			setAdjustments( data.items || [] );
			applySettlement( data.settlement );
			setNotice( { status: 'success', message: __( 'Settlement record cancelled.', 'conferencemanager' ) } );
			fetchItems();
		} catch ( err ) {
			setNotice( { status: 'error', message: err.message } );
		} finally {
			setDeletingId( null );
		}
	};

	const handleSettle = async () => {
		if ( ! settleModal ) return;

		// 🔴 目的が未選択・未知のまま実行させない。下の送信部は else が 'refund' なので、
		//    ここを通り抜けると「返金」として記録される。
		if ( ! SETTLE_PURPOSES.includes( settleForm.purpose ) ) {
			setNotice( { status: 'error', message: __( 'Select the purpose of this settlement first.', 'conferencemanager' ) } );
			return;
		}

		const isCorrection = 'correct_paid' === settleForm.purpose;
		const amount = parseInt( settleForm.amount, 10 );
		// ⚠️ 訂正は 0 を許す（入力欄の min="0" ともサーバとも揃える）。
		//    ここを一律 <= 0 にしていたため「実は 1 円も入っていなかった」を記録できなかった。
		if ( isNaN( amount ) || ( isCorrection ? amount < 0 : amount <= 0 ) ) {
			setNotice( {
				status: 'error',
				message: isCorrection
					? __( 'Enter the corrected paid amount as a number of 0 or more.', 'conferencemanager' )
					: __( 'Enter a settlement amount of 1 or more.', 'conferencemanager' ),
			} );
			return;
		}

		// 入金額の訂正は金銭の授受を伴わないため、精算明細ではなく記録の修正として扱う
		if ( isCorrection ) {
			// 🔴 これは記録の**上書き**。差額ではなく絶対額なので、
			//    取り違えると入金額そのものが壊れる。何から何に変えるかを見せてから実行する。
			const confirmText = sprintf(
				/* translators: 1: currently recorded amount, 2: new amount */
				__( 'Overwrite the recorded paid amount from %1$s to %2$s? No money changes hands; only the record is corrected.', 'conferencemanager' ),
				yen( paidAmount( settleModal ) ?? 0 ),
				yen( amount )
			);
			if ( ! window.confirm( confirmText ) ) {
				return;
			}
			setSettling( true );
			try {
				await apiFetch( {
					path: `/conf-manager/v1/payments/${ settleModal.id }/paid-amount`,
					method: 'PUT',
					data: { paid_amount: amount, reason: settleForm.reason },
				} );
				setNotice( { status: 'success', message: __( 'Paid amount corrected.', 'conferencemanager' ) } );
				setSettleModal( null );
				fetchItems();
			} catch ( err ) {
				setNotice( { status: 'error', message: err.message } );
			} finally {
				setSettling( false );
			}
			return;
		}

		const isCancel = 'refund_cancel' === settleForm.purpose;
		if ( isCancel ) {
			// ⚠️ v1.4.0 でカード決済は Pro 版へ移った。**カード会社への実返金は行わない**ので
			// 「Stripe を通じて返金する」とは言わない（言うと、返ってきていない金額を
			// 返ったと読ませることになる）。
			const warning = settleModal.method === 'stripe'
				? __( 'This will record a full refund and cancel the registration. Card payments are no longer refunded automatically — refund the payment in your payment provider yourself. Continue?', 'conferencemanager' )
				: __( 'This will refund the full amount and cancel the registration. The registration will be cancelled and this cannot be undone. Continue?', 'conferencemanager' );
			if ( ! window.confirm( warning ) ) {
				return;
			}
		}

		setSettling( true );
		try {
			await apiFetch( {
				path: `/conf-manager/v1/payments/${ settleModal.id }/adjustments`,
				method: 'POST',
				data: {
					type: 'surcharge' === settleForm.purpose ? 'surcharge' : 'refund',
					amount,
					method: settleForm.method,
					occurred_at: settleForm.occurred_at,
					reason: settleForm.reason,
					cancel_registration: isCancel,
				},
			} );
			setNotice( {
				status: 'success',
				message: isCancel
					? __( 'Refunded in full and cancelled the registration.', 'conferencemanager' )
					: __( 'Settlement recorded.', 'conferencemanager' ),
			} );
			setSettleModal( null );
			fetchItems();
		} catch ( err ) {
			setNotice( { status: 'error', message: err.message } );
		} finally {
			setSettling( false );
		}
	};

	const handleExportCsv = async () => {
		if ( ! eventFilter ) return;
		try {
			const response = await apiFetch( {
				path: `/conf-manager/v1/payments/export/${ eventFilter }`,
				parse: false,
			} );
			const blob = await response.blob();
			const url = URL.createObjectURL( blob );
			const a = document.createElement( 'a' );
			a.href = url;
			a.download = response.headers.get( 'Content-Disposition' )?.match( /filename="(.+)"/ )?.[ 1 ] || 'payments.csv';
			document.body.appendChild( a );
			a.click();
			document.body.removeChild( a );
			URL.revokeObjectURL( url );
		} catch ( err ) {
			// ⚠️ parse:false の失敗は Response がそのまま throw されるので err.message は undefined。
			setNotice( {
				status: 'error',
				message: await apiErrorMessage( err, __( 'Failed to export the CSV.', 'conferencemanager' ) ),
			} );
		}
	};

	const canEdit = useCan( 'conf_manager_edit_payments' );

	// 帳票の発行。POST なのは、帳票行が無ければ発番して作るため。
	const issueDocument = async ( paymentId, type ) => {
		setIssuingId( paymentId );
		setNotice( null );
		try {
			const data = await apiFetch( {
				path: `/conf-manager/v1/payments/${ paymentId }/${ type }-pdf`,
				method: 'POST',
			} );
			downloadBase64Pdf( data.pdf_b64, data.filename );
		} catch ( err ) {
			setNotice( { status: 'error', message: err.message } );
		} finally {
			setIssuingId( null );
		}
	};
	const canExport = useCan( 'conf_manager_export_payments' );

	const COLUMNS = [
		{ key: 'id', label: 'ID' },
		{ key: 'registration_id', label: __( 'Registration ID', 'conferencemanager' ) },
		{ key: 'member_name', label: __( 'Name', 'conferencemanager' ) },
		{ key: 'method', label: __( 'Payment Method', 'conferencemanager' ),
			render: ( val ) => METHOD_LABELS[ val ] || val,
		},
		{ key: 'amount', label: __( 'Billed', 'conferencemanager' ),
			render: ( _, row ) => yen( billedAmount( row ) ),
		},
		{ key: 'paid_amount', label: __( 'Paid amount', 'conferencemanager' ),
			// 入金確認で実額が記録されるまでは常に「—」。請求額 ¥0（請求不要）でも
			// 未消込のうちに ¥0 と表示すると「入金があった」と誤読されるため区別する。
			// 精算が済んだ分は差し引いた純入金額を表示する。
			render: ( _, row ) => {
				const paid = netPaid( row );
				if ( null === paid ) {
					return '—';
				}
				const adjusted = Number( row.refund_total || 0 ) > 0 || Number( row.surcharge_total || 0 ) > 0;
				return (
					<span title={ adjusted ? sprintf(
						/* translators: 1: originally received amount, 2: refunded total, 3: surcharged total */
						__( 'Received %1$s / Refunded %2$s / Surcharged %3$s', 'conferencemanager' ),
						// paid_amount は消込時に記録した「実際に受け取った額」そのもの。
						// 返金・追加徴収は net_paid 側で差し引き済みなので、ここで足し戻さない。
						yen( paidAmount( row ) ),
						yen( row.refund_total || 0 ),
						yen( row.surcharge_total || 0 )
					) : '' }>
						{ yen( paid ) }
						{ adjusted && <span style={ { color: '#757575', fontSize: '11px' } }> *</span> }
					</span>
				);
			},
		},
		{ key: 'paid_diff', label: __( 'Difference', 'conferencemanager' ),
			render: ( _, row ) => {
				const diff = paidDiff( row );
				if ( null === diff || 0 === diff ) {
					return '—';
				}
				return (
					<span
						style={ { color: diffColor( diff ), fontWeight: 'bold' } }
						title={ diff > 0 ? sprintf(
							/* translators: %s: overpaid amount */
							__( '%s more than the billed amount was received (refund due)', 'conferencemanager' ),
							yen( diff )
						) : sprintf(
							/* translators: %s: shortfall amount */
							__( '%s less than the billed amount was received (surcharge due)', 'conferencemanager' ),
							yen( Math.abs( diff ) )
						) }
					>
						{ diff < 0 ? `−${ yen( Math.abs( diff ) ) }` : `+${ yen( diff ) }` }
					</span>
				);
			},
		},
		{ key: 'status', label: __( 'Status', 'conferencemanager' ),
			render: ( _, row ) => {
				const { label, color } = resolveStatusDisplay( row );
				return (
					<span style={ { color, fontWeight: 'bold' } }>
						{ label }
					</span>
				);
			},
		},
		{ key: 'bank_transfer_name', label: __( 'Transfer Name', 'conferencemanager' ) },
		{ key: 'paid_at', label: __( 'Payment Date', 'conferencemanager' ),
			// paid_at は GMT 保存。サーバが添える paid_at_local（サイトのタイムゾーン）を出す。
			render: ( val, row ) => ( row.paid_at_local || '' ).substring( 0, 10 ),
		},
		{
			key: 'actions',
			label: __( 'Actions', 'conferencemanager' ),
			render: ( _, row ) => (
				<div style={ { display: 'flex', gap: '4px' } }>
					<Button variant="tertiary" isSmall onClick={ () => setDetailModal( row ) }>
						{ __( 'Details', 'conferencemanager' ) }
					</Button>
					{ canEdit && row.status === 'pending' && (
						<Button variant="primary" isSmall className="conf-manager-btn--action" onClick={ () => openConfirm( row ) }>
							{ __( 'Confirm Payment', 'conferencemanager' ) }
						</Button>
					) }
					{ canEdit && BILLABLE_STATUSES.includes( row.status ) && (
						<Button
							isSmall
							className="conf-manager-btn--mini conf-manager-btn--mini-blue"
							disabled={ issuingId === row.id }
							onClick={ () => issueDocument( row.id, 'invoice' ) }
						>
							{ __( 'Invoice', 'conferencemanager' ) }
						</Button>
					) }
					{ canEdit && RECEIPT_STATUSES.includes( row.status ) && (
						<Button
							isSmall
							className="conf-manager-btn--mini conf-manager-btn--mini-green"
							disabled={ issuingId === row.id }
							onClick={ () => issueDocument( row.id, 'receipt' ) }
						>
							{ __( 'Receipt', 'conferencemanager' ) }
						</Button>
					) }
					{ canEdit && row.status === 'completed' && (
						<Button
							variant={ [ 'refund_due', 'surcharge_due' ].includes( settlementState( row ) ) ? 'primary' : 'secondary' }
							isSmall
							className={ [ 'refund_due', 'surcharge_due' ].includes( settlementState( row ) ) ? 'conf-manager-btn--action' : undefined }
							onClick={ () => openSettle( row ) }
						>
							{ __( 'Settle', 'conferencemanager' ) }
						</Button>
					) }
				</div>
			),
		},
	];

	return (
		<div>
			<div className="conf-manager-page-header">
				<h2>{ __( 'Payments', 'conferencemanager' ) }</h2>
				{ canExport && eventFilter && (
					<Button variant="secondary" onClick={ handleExportCsv }>
						{ __( 'CSV Export', 'conferencemanager' ) }
					</Button>
				) }
			</div>

			<GuideSection
				hero={ {
					title: __( 'Streamline Payment Management', 'conferencemanager' ),
					description: __( 'Manage the entire payment flow — from bank transfer reconciliation to invoice issuance.', 'conferencemanager' ),
				} }
				cards={ [
					{
						icon: <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" strokeWidth="1.5"><polyline points="9 11 12 14 22 4" /><path d="M21 12v7a2 2 0 01-2 2H5a2 2 0 01-2-2V5a2 2 0 012-2h11" /></svg>,
						title: __( 'Payment Confirmation & Reconciliation', 'conferencemanager' ),
						description: __( 'Confirm bank transfer payments and update participant status.', 'conferencemanager' ),
						tips: [
							__( 'Record transferor name and confirmation date for reconciliation', 'conferencemanager' ),
							__( 'Automatically notify participants after payment confirmation', 'conferencemanager' ),
							__( 'Filter by event and status', 'conferencemanager' ),
						],
					},
					{
						icon: <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" strokeWidth="1.5"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /></svg>,
						title: __( 'Document Issuance', 'conferencemanager' ),
						description: __( 'Auto-generate invoices and receipts as PDFs, accessible from participants\' My Page.', 'conferencemanager' ),
						tips: [
							__( 'Invoices are auto-generated upon registration confirmation', 'conferencemanager' ),
							__( 'Receipts are available after payment confirmation', 'conferencemanager' ),
							__( 'Streamline accounting with CSV export', 'conferencemanager' ),
						],
					},
					{
						icon: <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" strokeWidth="1.5"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18" /><polyline points="17 6 23 6 23 12" /></svg>,
						title: __( 'Refund Handling', 'conferencemanager' ),
						description: __( 'Supports full and partial refunds with reason tracking.', 'conferencemanager' ),
						tips: [
							__( 'Enter refund amount and reason to process', 'conferencemanager' ),
							__( 'Specify amount for partial refunds', 'conferencemanager' ),
							__( 'Refund history is available in payment details', 'conferencemanager' ),
						],
					},
				] }
			/>

			{ notice && (
				<Notice status={ notice.status } onDismiss={ () => setNotice( null ) }>
					{ notice.message }
				</Notice>
			) }

			{ /* 検索フィルタ */ }
			<form onSubmit={ ( e ) => { e.preventDefault(); setPage( 1 ); fetchItems(); } } className="conf-manager-search">
				<SelectControl
					value={ eventFilter }
					onChange={ ( val ) => { setEventFilter( val ); setPage( 1 ); } }
					options={ [
						{ label: __( 'All Events', 'conferencemanager' ), value: '' },
						...events.map( ( ev ) => ( {
							label: `${ ev.title }（${ ev.date_start?.substring( 0, 10 ) }）`,
							value: String( ev.id ),
						} ) ),
					] }
				/>
				<SelectControl
					value={ statusFilter }
					onChange={ setStatusFilter }
					options={ [
						{ label: __( 'All Statuses', 'conferencemanager' ), value: '' },
						...Object.entries( STATUS_LABELS ).map( ( [ v, l ] ) => ( { label: l, value: v } ) ),
					] }
				/>
				<SelectControl
					value={ methodFilter }
					onChange={ setMethodFilter }
					options={ [
						{ label: __( 'All Methods', 'conferencemanager' ), value: '' },
						...Object.entries( METHOD_LABELS ).map( ( [ v, l ] ) => ( { label: l, value: v } ) ),
					] }
				/>
				<Button type="submit" className="conf-manager-btn--primary">
					{ __( 'Search', 'conferencemanager' ) }
				</Button>
			</form>

			{ /* 売上集計カード */ }
			{ summary && (
				<div className="conf-manager-payment-summary">
					{ ( () => {
						// 入金済カードは実入金額の合計。請求額と差がある場合のみ差額を併記する。
						const billed = summary.total.completed_amount + summary.total.completed_tax;
						const paid = summary.total.completed_paid ?? billed;
						const diff = paid - billed;
						return (
							<div className="conf-manager-payment-summary__card is-completed">
								<div className="conf-manager-payment-summary__label">{ __( 'Paid', 'conferencemanager' ) }</div>
								<div className="conf-manager-payment-summary__value">{ yen( paid ) }</div>
								<div className="conf-manager-payment-summary__count">{ summary.total.completed_count }{ __( 'items', 'conferencemanager' ) }</div>
								{ 0 !== diff && (
									<div style={ { fontSize: '12px', marginTop: '4px', color: diffColor( diff ) } }>
										{ sprintf(
											/* translators: 1: billed total, 2: signed difference */
											__( 'Billed %1$s / Difference %2$s', 'conferencemanager' ),
											yen( billed ),
											diff < 0 ? `−${ yen( Math.abs( diff ) ) }` : `+${ yen( diff ) }`
										) }
									</div>
								) }
							</div>
						);
					} )() }
					<div className="conf-manager-payment-summary__card is-pending">
						<div className="conf-manager-payment-summary__label">{ __( 'Unpaid', 'conferencemanager' ) }</div>
						<div className="conf-manager-payment-summary__value">&yen;{ ( summary.total.pending_amount + summary.total.pending_tax ).toLocaleString() }</div>
						<div className="conf-manager-payment-summary__count">{ summary.total.pending_count }{ __( 'items', 'conferencemanager' ) }</div>
					</div>
					{ ( () => {
						// 表示中のページから精算が必要な件数・金額を集計する
						const due = items.filter( ( r ) => [ 'refund_due', 'surcharge_due' ].includes( settlementState( r ) ) );
						if ( 0 === due.length ) {
							return null;
						}
						const refund = due.filter( ( r ) => 'refund_due' === settlementState( r ) )
							.reduce( ( s, r ) => s + ( paidDiff( r ) || 0 ), 0 );
						const surcharge = due.filter( ( r ) => 'surcharge_due' === settlementState( r ) )
							.reduce( ( s, r ) => s + Math.abs( paidDiff( r ) || 0 ), 0 );
						return (
							<div className="conf-manager-payment-summary__card is-settlement">
								<div className="conf-manager-payment-summary__label">{ __( 'Needs settlement', 'conferencemanager' ) }</div>
								<div className="conf-manager-payment-summary__value">{ due.length }{ __( 'items', 'conferencemanager' ) }</div>
								{ refund > 0 && (
									<div style={ { fontSize: '12px', marginTop: '4px', color: SETTLEMENT_COLORS.refund_due } }>
										{ __( 'Refund due', 'conferencemanager' ) }: { yen( refund ) }
									</div>
								) }
								{ surcharge > 0 && (
									<div style={ { fontSize: '12px', marginTop: '2px', color: SETTLEMENT_COLORS.surcharge_due } }>
										{ __( 'Surcharge due', 'conferencemanager' ) }: { yen( surcharge ) }
									</div>
								) }
							</div>
						);
					} )() }
					{ /* ⚠️ 手数料は過去のカード決済にしか付かない。0 のときは出さない
					     （カード決済が無いのに「手数料 ¥0」の枠だけ残ると意味が分からない）。 */ }
					{ ( summary.total.completed_fee || 0 ) > 0 && (
						<div className="conf-manager-payment-summary__card">
							<div className="conf-manager-payment-summary__label">{ __( 'Payment Fees', 'conferencemanager' ) }</div>
							<div className="conf-manager-payment-summary__value">&yen;{ ( summary.total.completed_fee || 0 ).toLocaleString() }</div>
						</div>
					) }
					{ summary.by_method && Object.keys( summary.by_method ).length > 1 && (
						<div className="conf-manager-payment-summary__card">
							<div className="conf-manager-payment-summary__label">{ __( 'Breakdown by Method', 'conferencemanager' ) }</div>
							{ Object.entries( summary.by_method ).map( ( [ method, data ] ) => (
								<div key={ method } style={ { fontSize: '13px', marginTop: '4px' } }>
									{ METHOD_LABELS[ method ] || method }: { yen( data.paid ?? ( data.amount + data.tax ) ) } ({ data.count }{ __( 'items', 'conferencemanager' ) })
								</div>
							) ) }
						</div>
					) }
				</div>
			) }

			<DataTable
				columns={ COLUMNS }
				rows={ items }
				total={ total }
				page={ page }
				perPage={ 20 }
				onPageChange={ setPage }
				loading={ loading }
			/>

			{ /* 入金確認モーダル */ }
			{ confirmModal && (
				<Modal
					title={ __( 'Confirm Payment', 'conferencemanager' ) }
					onRequestClose={ () => setConfirmModal( null ) }
				>
					<p>
						{ __( 'Payment ID:', 'conferencemanager' ) } { confirmModal.id } /
						{ ' ' }{ __( 'Name:', 'conferencemanager' ) } { confirmModal.member_name } /
						{ ' ' }{ __( 'Billed:', 'conferencemanager' ) } { yen( billedAmount( confirmModal ) ) }
					</p>
					{ confirmModal.transfer_note && (
						<p style={ { padding: '8px 12px', marginBottom: '12px', background: '#fcf9e8', borderLeft: '4px solid #dba617' } }>
							<strong>{ __( 'Transfer Account Name Note', 'conferencemanager' ) }</strong>
							<br />{ confirmModal.transfer_note }
						</p>
					) }
					<TextControl
						label={ __( 'Transfer Name', 'conferencemanager' ) }
						value={ confirmName }
						onChange={ setConfirmName }
					/>
					<TextControl
						label={ __( 'Transfer Date', 'conferencemanager' ) }
						type="date"
						value={ confirmDate }
						onChange={ setConfirmDate }
						help={ __( 'The date the payer sent the transfer (as they reported it).', 'conferencemanager' ) }
					/>
					<TextControl
						label={ __( 'Payment Date', 'conferencemanager' ) }
						type="date"
						value={ confirmPaidAt }
						onChange={ setConfirmPaidAt }
						help={ __( 'The date the money arrived in your account (as shown on your bank statement), not the date you checked it. This is what appears on the invoice and receipt.', 'conferencemanager' ) }
					/>
					<TextControl
						label={ __( 'Paid amount (incl. tax)', 'conferencemanager' ) }
						type="number"
						min="0"
						value={ confirmPaidAmount }
						onChange={ setConfirmPaidAmount }
						help={ __( 'The amount actually received. Leave as-is if it matches the billed amount (adjust when, for example, a transfer fee was deducted).', 'conferencemanager' ) }
					/>
					{ ( () => {
						const paid = parseInt( confirmPaidAmount, 10 );
						if ( isNaN( paid ) ) {
							return null;
						}
						const diff = paid - billedAmount( confirmModal );
						if ( 0 === diff ) {
							return (
								<p style={ { fontSize: '13px', color: '#00a32a', fontWeight: 'bold' } }>
									{ __( 'Matches the billed amount.', 'conferencemanager' ) }
								</p>
							);
						}
						return (
							<p style={ { fontSize: '13px', color: diffColor( diff ), fontWeight: 'bold' } }>
								{ diff < 0
									? sprintf(
										/* translators: %s: shortfall amount */
										__( 'Underpaid: %s', 'conferencemanager' ),
										yen( Math.abs( diff ) )
									)
									: sprintf(
										/* translators: %s: overpaid amount */
										__( 'Overpaid: %s', 'conferencemanager' ),
										yen( diff )
									) }
							</p>
						);
					} )() }
					<TextareaControl
						label={ __( 'Handover note (optional)', 'conferencemanager' ) }
						value={ confirmNote }
						onChange={ setConfirmNote }
						rows={ 2 }
						help={ __( 'Notes from the person confirming the payment to the reception desk or accounting. Example: transfer fee deducted, shortfall to be collected at the desk.', 'conferencemanager' ) }
					/>
					<div style={ { marginTop: '16px', display: 'flex', gap: '8px' } }>
						<Button variant="primary" onClick={ handleConfirm }>
							{ __( 'Confirm Payment', 'conferencemanager' ) }
						</Button>
						<Button variant="tertiary" onClick={ () => setConfirmModal( null ) }>
							{ __( 'Cancel', 'conferencemanager' ) }
						</Button>
					</div>
				</Modal>
			) }

			{ /* 精算モーダル */ }
			{ settleModal && ( () => {
				const billed = billedAmount( settleModal );
				const net = netPaid( settleModal );
				const diff = paidDiff( settleModal );
				const state = settlementState( settleModal );
				// 目的を選ぶまでは金額欄も実行ボタンも出さない（A の直し）
				const chosen = SETTLE_PURPOSES.includes( settleForm.purpose );
				const isCorrection = 'correct_paid' === settleForm.purpose;
				const isDeleteMode = 'delete_adjustment' === settleForm.purpose;
				return (
					<Modal
						title={ __( 'Settlement', 'conferencemanager' ) }
						onRequestClose={ () => setSettleModal( null ) }
					>
						<p style={ { color: '#50575e' } }>
							{ __( 'Payment ID:', 'conferencemanager' ) } { settleModal.id } /
							{ ' ' }{ settleModal.member_name }
						</p>
						<dl style={ { display: 'grid', gridTemplateColumns: '120px 1fr', gap: '4px 12px', margin: '8px 0 12px' } }>
							<dt style={ { color: '#50575e' } }>{ __( 'Billed', 'conferencemanager' ) }</dt>
							<dd style={ { margin: 0 } }>{ yen( billed ) }</dd>
							<dt style={ { color: '#50575e' } }>{ __( 'Net paid', 'conferencemanager' ) }</dt>
							<dd style={ { margin: 0 } }>{ null === net ? '—' : yen( net ) }</dd>
							<dt style={ { color: '#50575e' } }>{ __( 'Difference', 'conferencemanager' ) }</dt>
							<dd style={ { margin: 0, fontWeight: 'bold', color: SETTLEMENT_COLORS[ state ] || 'inherit' } }>
								{ null === diff || 0 === diff
									? '—'
									: ( diff > 0 ? `+${ yen( diff ) }` : `−${ yen( Math.abs( diff ) ) }` ) }
								{ SETTLEMENT_LABELS[ state ] && ` (${ SETTLEMENT_LABELS[ state ] })` }
							</dd>
						</dl>

						{ settleModal.confirm_note && (
							<Notice status="info" isDismissible={ false }>
								<strong>{ __( 'Handover note from payment confirmation', 'conferencemanager' ) }</strong>
								<div style={ { whiteSpace: 'pre-wrap' } }>{ settleModal.confirm_note }</div>
							</Notice>
						) }

						{ adjustments.length > 0 && (
							<>
								<h3 style={ { fontSize: '13px', margin: '12px 0 6px' } }>{ __( 'Settlement history', 'conferencemanager' ) }</h3>
								<table className="widefat striped" style={ { marginBottom: '12px' } }>
									<thead>
										<tr>
											<th>{ __( 'Date', 'conferencemanager' ) }</th>
											<th>{ __( 'Type', 'conferencemanager' ) }</th>
											<th>{ __( 'Amount', 'conferencemanager' ) }</th>
											<th>{ __( 'Method', 'conferencemanager' ) }</th>
											<th>{ __( 'Handled by', 'conferencemanager' ) }</th>
											{ isDeleteMode && <th>{ __( 'Cancel record', 'conferencemanager' ) }</th> }
										</tr>
									</thead>
									<tbody>
										{ adjustments.map( ( a ) => (
											<tr key={ a.id }>
												<td>{ ( a.occurred_at || '' ).substring( 0, 16 ) }</td>
												<td>{ 'refund' === a.type ? __( 'Refund', 'conferencemanager' ) : __( 'Surcharge', 'conferencemanager' ) }</td>
												<td>{ yen( a.amount ) }</td>
												<td>{ ADJUSTMENT_METHODS.find( ( m ) => m.value === a.method )?.label || a.method }</td>
												<td>{ a.handled_by_name || '—' }</td>
												{ isDeleteMode && (
													<td>
														{ /* 🔴 可否はサーバが返す can_delete をそのまま使う。
														     画面側で条件を書き直すと、押せるのに断られる／
														     取り消せるのにボタンが出ない、のどちらかになる。 */ }
														{ a.can_delete ? (
															<Button
																variant="secondary"
																isSmall
																isDestructive
																isBusy={ deletingId === a.id }
																disabled={ null !== deletingId }
																onClick={ () => handleDeleteAdjustment( a ) }
															>
																{ /* ⚠️ ここに 'Cancel'（キャンセル）を使わない。
																     モーダルを閉じるボタンと同じ語になり、
																     「やめる」と読めてしまう（実機で気づいた）。 */ }
																{ __( 'Cancel this record', 'conferencemanager' ) }
															</Button>
														) : (
															<span style={ { fontSize: '11px', color: '#757575' } } title={ a.delete_blocked_note || '' }>
																{ __( 'Cannot be cancelled', 'conferencemanager' ) }
															</span>
														) }
													</td>
												) }
											</tr>
										) ) }
									</tbody>
								</table>
							</>
						) }

						{ /* 🔴 目的は 2 群に分ける。「お金が動いた」と「記録を直す」は
						     まったく別の操作なのに、並べて置くと同じ重さに見える。 */ }
						<SelectControl
							label={ __( 'Purpose of settlement', 'conferencemanager' ) }
							value={ settleForm.purpose }
							onChange={ ( v ) => setSettleForm( ( p ) => ( {
								...p,
								purpose: v,
								// ⚠️ 目的が変わったら金額は**必ず**入れ直す（残すと別の意味で送られる）
								amount: defaultSettleAmount( settleModal, v ),
							} ) ) }
						>
							<option value="">{ __( 'Select a purpose', 'conferencemanager' ) }</option>
							<optgroup label={ __( 'Money actually changed hands', 'conferencemanager' ) }>
								<option value="refund_diff">
									{ null !== diff && diff > 0
										? sprintf(
											/* translators: %s: overpaid amount */
											__( 'Refunded the participant (overpaid %s)', 'conferencemanager' ),
											yen( diff )
										)
										: __( 'Refunded the participant', 'conferencemanager' ) }
								</option>
								<option value="surcharge">
									{ null !== diff && diff < 0
										? sprintf(
											/* translators: %s: shortfall amount */
											__( 'Received an additional payment (shortfall %s)', 'conferencemanager' ),
											yen( Math.abs( diff ) )
										)
										: __( 'Received an additional payment', 'conferencemanager' ) }
								</option>
								<option value="refund_cancel">{ __( 'Refunded in full and cancelled the registration', 'conferencemanager' ) }</option>
							</optgroup>
							<optgroup label={ __( 'No money moves: correct the record', 'conferencemanager' ) }>
								<option value="correct_paid">
									{ sprintf(
										/* translators: %s: currently recorded paid amount */
										__( 'Correct the recorded paid amount (currently %s)', 'conferencemanager' ),
										yen( paidAmount( settleModal ) ?? 0 )
									) }
								</option>
								<option value="delete_adjustment">{ __( 'Cancel a settlement record entered by mistake', 'conferencemanager' ) }</option>
							</optgroup>
						</SelectControl>
						{ isDeleteMode && (
							<Notice status="info" isDismissible={ false }>
								{ 0 === adjustments.length
									? __( 'There is no settlement record to cancel.', 'conferencemanager' )
									: __( 'Use the Cancel button in the settlement history above. The record is removed from the history and from the totals, and its details are kept in the audit log. A refund that cancelled the registration cannot be cancelled here.', 'conferencemanager' ) }
							</Notice>
						) }
						{ isCorrection && (
							<Notice status="info" isDismissible={ false }>
								{ __( 'Corrects the recorded paid amount to the amount actually received. It is not added to the refund or surcharge history; the old and new values are written to the audit log.', 'conferencemanager' ) }
							</Notice>
						) }
						{ 'refund_cancel' === settleForm.purpose && (
							<Notice status="warning" isDismissible={ false }>
								{ settleModal.method === 'stripe'
									? __( 'The registration will be cancelled and the refund recorded. Card payments are no longer refunded automatically — refund the payment in your payment provider yourself.', 'conferencemanager' )
									: __( 'The registration will be cancelled. This cannot be undone. The actual refund must be handled separately.', 'conferencemanager' ) }
							</Notice>
						) }
						{ /* 🔴 目的を選ぶまでは金額欄も手段も実行ボタンも 1 つも描かない。
						     開いた直後に押せる状態にしておかないこと自体が、この画面の直しの本体。 */ }
						{ chosen && (
							<>
								{ isCorrection && (
									<p style={ { margin: '8px 0 0', fontSize: '13px' } }>
										{ sprintf(
											/* translators: %s: currently recorded paid amount */
											__( 'Current record: %s', 'conferencemanager' ),
											yen( paidAmount( settleModal ) ?? 0 )
										) }
									</p>
								) }
								<TextControl
									label={ isCorrection
										? __( 'New record (correct paid amount, incl. tax)', 'conferencemanager' )
										: __( 'Settlement amount (incl. tax)', 'conferencemanager' ) }
									type="number"
									min={ isCorrection ? '0' : '1' }
									value={ settleForm.amount }
									onChange={ ( v ) => setSettleForm( ( p ) => ( { ...p, amount: v } ) ) }
									help={ isCorrection
										? __( 'This is not the difference. Enter the total amount actually received.', 'conferencemanager' )
										: __( 'Enter the amount that changed hands at the desk, not the new total.', 'conferencemanager' ) }
								/>
								{ /* 記録の訂正は金銭の授受を伴わないため、手段・実施日は入力させない */ }
								{ ! isCorrection && (
									<>
										<SelectControl
											label={ __( 'Settlement method', 'conferencemanager' ) }
											value={ settleForm.method }
											onChange={ ( v ) => setSettleForm( ( p ) => ( { ...p, method: v } ) ) }
											options={ ADJUSTMENT_METHODS }
										/>
										<TextControl
											label={ __( 'Date handled', 'conferencemanager' ) }
											type="date"
											value={ settleForm.occurred_at }
											onChange={ ( v ) => setSettleForm( ( p ) => ( { ...p, occurred_at: v } ) ) }
											help={ __( 'If left blank, the current date and time is recorded.', 'conferencemanager' ) }
										/>
									</>
								) }
								<TextControl
									label={ __( 'Reason', 'conferencemanager' ) }
									value={ settleForm.reason }
									onChange={ ( v ) => setSettleForm( ( p ) => ( { ...p, reason: v } ) ) }
									placeholder={ isCorrection
										? __( 'Example: corrected to the actual amount because a transfer fee was deducted', 'conferencemanager' )
										: __( 'Example: refund of the difference due to an incorrect membership category', 'conferencemanager' ) }
								/>
								<p style={ { fontSize: '12px', color: '#757575' } }>
									{ __( 'The handler and timestamp are recorded automatically. Refunds and surcharges are kept as history and reflected in the totals.', 'conferencemanager' ) }
								</p>
							</>
						) }
						<div style={ { marginTop: '16px', display: 'flex', gap: '8px' } }>
							{ chosen && (
								<Button variant="primary" isBusy={ settling } disabled={ settling } onClick={ handleSettle }>
									{ isCorrection
										? __( 'Correct paid amount', 'conferencemanager' )
										: __( 'Record settlement', 'conferencemanager' ) }
								</Button>
							) }
							<Button variant="tertiary" onClick={ () => setSettleModal( null ) }>
								{ __( 'Close', 'conferencemanager' ) }
							</Button>
						</div>
					</Modal>
				);
			} )() }

			{ /* 詳細モーダル */ }
			{ detailModal && (
				<Modal
					title={ __( 'Payment Details', 'conferencemanager' ) }
					onRequestClose={ () => setDetailModal( null ) }
				>
					<dl style={ { display: 'grid', gridTemplateColumns: '140px 1fr', gap: '8px 12px', margin: 0 } }>
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Payment ID', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0 } }>{ detailModal.id }</dd>
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Registration ID', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0 } }>{ detailModal.registration_id }</dd>
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Name', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0 } }>{ detailModal.member_name }</dd>
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Payment Method', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0 } }>{ METHOD_LABELS[ detailModal.method ] || detailModal.method }</dd>
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Amount (excl. tax)', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0 } }>&yen;{ Number( detailModal.amount ).toLocaleString() }</dd>
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Tax', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0 } }>&yen;{ Number( detailModal.tax || 0 ).toLocaleString() }</dd>
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Billed (incl. tax)', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0, fontWeight: 'bold' } }>{ yen( billedAmount( detailModal ) ) }</dd>
						{ detailModal.fee > 0 && (
							<>
								<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Fees', 'conferencemanager' ) }</dt>
								<dd style={ { margin: 0 } }>&yen;{ Number( detailModal.fee ).toLocaleString() }</dd>
							</>
						) }
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Paid (incl. tax)', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0, fontWeight: 'bold' } }>
							{ null === paidAmount( detailModal ) ? '—' : yen( paidAmount( detailModal ) ) }
						</dd>
						{ null !== paidDiff( detailModal ) && 0 !== paidDiff( detailModal ) && (
							<>
								<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Difference', 'conferencemanager' ) }</dt>
								<dd style={ { margin: 0, fontWeight: 'bold', color: diffColor( paidDiff( detailModal ) ) } }>
									{ paidDiff( detailModal ) < 0
										? `−${ yen( Math.abs( paidDiff( detailModal ) ) ) }`
										: `+${ yen( paidDiff( detailModal ) ) }` }
								</dd>
							</>
						) }
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Status', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0 } }>
							<span style={ { color: resolveStatusDisplay( detailModal ).color, fontWeight: 'bold' } }>
								{ resolveStatusDisplay( detailModal ).label }
							</span>
						</dd>
						{ detailModal.gateway_txn_id && (
							<>
								<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Transaction ID', 'conferencemanager' ) }</dt>
								<dd style={ { margin: 0, fontSize: '12px', wordBreak: 'break-all' } }>{ detailModal.gateway_txn_id }</dd>
							</>
						) }
						{ detailModal.confirm_note && (
							<>
								<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Handover note', 'conferencemanager' ) }</dt>
								<dd style={ { margin: 0, whiteSpace: 'pre-wrap' } }>{ detailModal.confirm_note }</dd>
							</>
						) }
						{ detailModal.bank_transfer_name && (
							<>
								<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Transfer Name', 'conferencemanager' ) }</dt>
								<dd style={ { margin: 0 } }>{ detailModal.bank_transfer_name }</dd>
							</>
						) }
						{ detailModal.refund_amount > 0 && (
							<>
								<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Refund Amount', 'conferencemanager' ) }</dt>
								<dd style={ { margin: 0, color: '#d63638' } }>&yen;{ Number( detailModal.refund_amount ).toLocaleString() }</dd>
								<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Refund Reason', 'conferencemanager' ) }</dt>
								<dd style={ { margin: 0 } }>{ detailModal.refund_reason || '-' }</dd>
							</>
						) }
						<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Created', 'conferencemanager' ) }</dt>
						<dd style={ { margin: 0 } }>{ detailModal.created_at_local || detailModal.created_at }</dd>
						{ detailModal.paid_at && (
							<>
								<dt style={ { fontWeight: 600, color: '#50575e' } }>{ __( 'Payment Date', 'conferencemanager' ) }</dt>
								<dd style={ { margin: 0 } }>{ detailModal.paid_at_local || detailModal.paid_at }</dd>
							</>
						) }
					</dl>
					<div style={ { marginTop: '16px' } }>
						<Button variant="tertiary" onClick={ () => setDetailModal( null ) }>
							{ __( 'Close', 'conferencemanager' ) }
						</Button>
					</div>
				</Modal>
			) }
		</div>
	);
}
