Spaces:
Runtime error
Runtime error
| import { motion } from 'framer-motion' | |
| import { useStatement } from '../store/statementStore' | |
| import { useNav } from '../store/navStore' | |
| import type { StatementCell } from '../types' | |
| import { EventCard } from './EventCard' | |
| import { SpinBox } from './SpinBox' | |
| export function StatementScreen() { | |
| const { phase, problem, inputs, result, setInput, submit, retry, backToSelect } = useStatement() | |
| const { setSection } = useNav() | |
| if (!problem) return null | |
| const isResult = phase === 'result' | |
| return ( | |
| <div className="mx-auto max-w-4xl px-4 py-5"> | |
| <div className="mb-4 flex items-center justify-between"> | |
| <button | |
| onClick={() => { backToSelect(); setSection('journal') }} | |
| className="text-sm text-slate-400 hover:text-slate-200" | |
| > | |
| ← 論点選択 | |
| </button> | |
| {result && ( | |
| <span className={`rounded-full px-3 py-1 text-xs font-bold ${result.correct ? 'bg-emerald-500/20 text-emerald-300' : 'bg-rose-500/20 text-rose-300'}`}> | |
| {result.score} / {result.total} 正解 | |
| </span> | |
| )} | |
| </div> | |
| {/* 問題文・試算表資料 */} | |
| <EventCard | |
| problemId={problem.id} | |
| eventText={problem.event_text} | |
| hint={problem.hint} | |
| documents={problem.documents} | |
| sectionLabel="第3問 財務諸表" | |
| topicLabel="貸借対照表の作成" | |
| /> | |
| {/* 貸借対照表 */} | |
| <div className="mt-4 rounded-2xl border border-slate-700 bg-slate-900/60 overflow-x-auto"> | |
| <div className="p-3 text-center text-sm font-bold text-slate-200">貸 借 対 照 表</div> | |
| <table className="w-full text-sm"> | |
| <thead> | |
| <tr className="border-y border-slate-700"> | |
| <th className="px-3 py-1.5 text-left text-xs text-sky-300">資産の部</th> | |
| <th className="w-40 px-3 py-1.5 text-right text-xs text-sky-300">金額</th> | |
| <th className="w-2 bg-slate-700/50" /> | |
| <th className="px-3 py-1.5 text-left text-xs text-amber-300">負債・純資産の部</th> | |
| <th className="w-40 px-3 py-1.5 text-right text-xs text-amber-300">金額</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {problem.bs_rows.map((row, ri) => { | |
| const leftOk = result ? result.rowResults.find((r) => r.rowIndex === ri)?.leftOk ?? true : null | |
| const rightOk = result ? result.rowResults.find((r) => r.rowIndex === ri)?.rightOk ?? true : null | |
| return ( | |
| <tr key={ri} className="border-b border-slate-700/50"> | |
| {/* 左: ラベル */} | |
| <td className={`px-3 py-2 ${leftOk === false ? 'bg-rose-500/5' : ''}`}> | |
| {renderLabelCell(row.left.label, `bs_${ri}_left_label`, inputs, setInput, isResult)} | |
| </td> | |
| {/* 左: 金額 */} | |
| <td className={`px-3 py-2 text-right ${leftOk === false ? 'bg-rose-500/5' : ''}`}> | |
| {renderAmountCell(row.left.amount, `bs_${ri}_left_amount`, `bs_${ri}_left_amount2`, inputs, setInput, isResult)} | |
| </td> | |
| <td className="bg-slate-700/50" /> | |
| {/* 右: ラベル */} | |
| <td className={`px-3 py-2 ${rightOk === false ? 'bg-rose-500/5' : ''}`}> | |
| {renderLabelCell(row.right.label, `bs_${ri}_right_label`, inputs, setInput, isResult)} | |
| </td> | |
| {/* 右: 金額 */} | |
| <td className={`px-3 py-2 text-right ${rightOk === false ? 'bg-rose-500/5' : ''}`}> | |
| {renderAmountCell(row.right.amount, `bs_${ri}_right_amount`, `bs_${ri}_right_amount2`, inputs, setInput, isResult)} | |
| </td> | |
| </tr> | |
| ) | |
| })} | |
| </tbody> | |
| </table> | |
| </div> | |
| {/* 結果 */} | |
| {isResult && result && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 8 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className={`mt-4 rounded-2xl border p-5 ${result.correct ? 'border-emerald-400/50 bg-emerald-500/10' : 'border-rose-400/50 bg-rose-500/10'}`} | |
| > | |
| <div className="mb-2 flex items-center gap-2"> | |
| <span className="text-2xl">{result.correct ? '🎉' : '🤔'}</span> | |
| <span className={`text-xl font-bold ${result.correct ? 'text-emerald-300' : 'text-rose-300'}`}> | |
| {result.correct ? '正解!' : `${result.score} / ${result.total} 問正解`} | |
| </span> | |
| </div> | |
| <div className="rounded-lg bg-slate-900/40 p-3 text-sm leading-relaxed text-slate-200"> | |
| <p className="mb-1 font-semibold text-cyan-300">解説</p> | |
| {problem.explanation} | |
| </div> | |
| <div className="mt-4 flex gap-3"> | |
| <button onClick={retry} className="flex-1 rounded-xl bg-cyan-500 py-2 font-bold text-slate-900 hover:bg-cyan-400"> | |
| もう一度 | |
| </button> | |
| <button onClick={() => { backToSelect(); setSection('journal') }} className="rounded-xl border border-slate-600 px-4 py-2 text-slate-300 hover:bg-slate-700"> | |
| 論点選択 | |
| </button> | |
| </div> | |
| </motion.div> | |
| )} | |
| {!isResult && ( | |
| <button | |
| onClick={submit} | |
| className="mt-4 w-full rounded-2xl bg-emerald-500 py-4 text-lg font-black text-slate-900 shadow-lg transition hover:bg-emerald-400" | |
| > | |
| 採点する ✓ | |
| </button> | |
| )} | |
| </div> | |
| ) | |
| } | |
| function renderLabelCell( | |
| cell: StatementCell, | |
| key: string, | |
| inputs: Record<string, string | number>, | |
| setInput: (k: string, v: string | number) => void, | |
| locked: boolean, | |
| ): React.ReactNode { | |
| if (cell.type === 'given_label') { | |
| if (!cell.value) return <span /> | |
| return <span className="text-slate-200">{String(cell.value)}</span> | |
| } | |
| if (cell.type === 'blank_account') { | |
| return ( | |
| <span className="flex items-center gap-1"> | |
| <input | |
| type="text" | |
| value={String(inputs[key] ?? '')} | |
| disabled={locked} | |
| onChange={(e) => setInput(key, e.target.value)} | |
| placeholder="( )" | |
| className="w-16 rounded border border-slate-500 bg-slate-900/70 px-2 py-0.5 text-slate-100 outline-none focus:border-cyan-400 disabled:opacity-60" | |
| /> | |
| {cell.suffix && <span className="text-slate-200">{cell.suffix}</span>} | |
| </span> | |
| ) | |
| } | |
| return null | |
| } | |
| function renderAmountCell( | |
| cell: StatementCell, | |
| key: string, | |
| key2: string, | |
| inputs: Record<string, string | number>, | |
| setInput: (k: string, v: string | number) => void, | |
| locked: boolean, | |
| ): React.ReactNode { | |
| if (cell.type === 'given_amount') { | |
| if (cell.value === null || cell.value === undefined) return <span /> | |
| return <span className="tabular-nums text-slate-200">¥{Number(cell.value).toLocaleString()}</span> | |
| } | |
| if (cell.type === 'blank_amount') { | |
| return ( | |
| <SpinBox | |
| inputMode="numeric" | |
| value={inputs[key] ?? ''} | |
| disabled={locked} | |
| onChange={(e) => setInput(key, Number(e.target.value))} | |
| placeholder="0" | |
| className="w-32 rounded border border-slate-500 bg-slate-900/70 px-2 py-0.5 text-right tabular-nums text-slate-100 outline-none focus:border-cyan-400 disabled:opacity-60" | |
| /> | |
| ) | |
| } | |
| if (cell.type === 'blank_contra') { | |
| return ( | |
| <span className="flex items-center justify-end gap-1"> | |
| <span className="text-rose-300">△</span> | |
| <SpinBox | |
| inputMode="numeric" | |
| value={inputs[key] ?? ''} | |
| disabled={locked} | |
| onChange={(e) => setInput(key, Number(e.target.value))} | |
| placeholder="0" | |
| className="w-24 rounded border border-slate-500 bg-slate-900/70 px-2 py-0.5 text-right tabular-nums text-slate-100 outline-none focus:border-cyan-400 disabled:opacity-60" | |
| /> | |
| {cell.answer2 !== undefined && ( | |
| <> | |
| <span className="text-slate-500">(</span> | |
| <SpinBox | |
| inputMode="numeric" | |
| value={inputs[key2] ?? ''} | |
| disabled={locked} | |
| onChange={(e) => setInput(key2, Number(e.target.value))} | |
| placeholder="純額" | |
| className="w-24 rounded border border-slate-500 bg-slate-900/70 px-2 py-0.5 text-right tabular-nums text-slate-100 outline-none focus:border-cyan-400 disabled:opacity-60" | |
| /> | |
| <span className="text-slate-500">)</span> | |
| </> | |
| )} | |
| </span> | |
| ) | |
| } | |
| return null | |
| } | |