連結精算表のUI改善とデータバグ修正
連結精算表コンポーネント(WorksheetTable.vue)周辺で、矢印線UI、個別財務諸表の多列テーブル化、データバグ修正、セクションヘッダーのスタイル改善を実施した。作業の記録をまとめる。
1. PL→BS 矢印接続線の実装
やりたかったこと
連結精算表では、P/Lの「当期純利益」がB/Sの利益剰余金内「当期純利益」行に流れる関係がある。この対応関係を視覚的に示すために、P/LからB/Sへの矢印接続線を描画したかった。
最初のアプローチ: 行のクラスで実装
最初は各<tr>にCSSクラス(arrow-source, arrow-line, arrow-target)を付与して、行自体の::before/::after疑似要素で縦線・矢じりを描こうとした。
問題: 行の疑似要素は勘定科目名のテキストと重なってしまう。position: absoluteで配置しても、テーブルセルの外にはみ出す部分がクリッピングされてしまう。
解決策: 矢印専用列(col-arrow)の追加
テーブルの先頭に幅20pxの専用列を追加し、その列のセル(<td class="col-arrow">)に対して疑似要素を描画する方式に変更した。
<!-- WorksheetTable.vue テンプレート -->
<thead>
<tr>
<th class="col-arrow"></th>
<th class="col-section">区分</th>
<th class="col-name">勘定科目</th>
<!-- ...数値列 -->
</tr>
</thead>
<tbody>
<tr :class="arrowRoles.get(`${sectionRows.key}-${rIdx}`) || ''">
<td class="col-arrow"></td>
<td class="col-section">...</td>
<td class="col-name">...</td>
<!-- ...数値セル -->
</tr>
</tbody>
矢印ロールの算出ロジック
各行にどの矢印パーツを描画するかをcomputedで算出する。
type ArrowRole = 'arrow-source' | 'arrow-target' | 'arrow-line' | ''
const arrowRoles = computed(() => {
const roles = new Map<string, ArrowRole>()
// B/Sの「当期純利益」行 = ターゲット(矢じりの先)
const bsTargetIdx = bsRows.value.findIndex(r => r.name === '当期純利益')
// P/Lの「親会社株主に帰属する当期純利益」行 = ソース(黒丸の起点)
let plSourceIdx = plRows.value.findIndex(
r => r.name === '親会社株主に帰属する当期純利益' && r.isSummary
)
if (plSourceIdx === -1) {
plSourceIdx = plRows.value.findIndex(r => r.name === '当期純利益' && r.isSummary)
}
if (bsTargetIdx === -1 || plSourceIdx === -1) return roles
// ターゲット行: L字型接続 + 矢じり三角
roles.set(`bs-${bsTargetIdx}`, 'arrow-target')
// ターゲットの下~P/Lセクションヘッダーまで: 縦線
for (let i = bsTargetIdx + 1; i < bsRows.value.length; i++) {
roles.set(`bs-${i}`, 'arrow-line')
}
roles.set('pl-section-header', 'arrow-line')
// P/Lの先頭~ソース行の前まで: 縦線
for (let i = 0; i < plSourceIdx; i++) {
roles.set(`pl-${i}`, 'arrow-line')
}
// ソース行: 縦線の端 + 黒丸
roles.set(`pl-${plSourceIdx}`, 'arrow-source')
return roles
})
CSSの描画
矢印は4つのパーツで構成される。
ソース(黒丸・起点):
/* 縦線(上半分まで) */
.arrow-source > td.col-arrow::before {
content: '';
position: absolute;
left: 3px;
top: 0;
height: 50%;
width: 2px;
background: #4b5563;
}
/* 黒丸 */
.arrow-source > td.col-arrow::after {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8px;
height: 8px;
border-radius: 50%;
background: #4b5563;
}
接続線(縦線):
.arrow-line > td.col-arrow::before {
content: '';
position: absolute;
left: 3px;
top: 0;
bottom: 0;
width: 2px;
background: #4b5563;
}
ターゲット(L字接続 + 矢じり三角):
/* L字型(縦線→横線) */
.arrow-target > td.col-arrow::before {
content: '';
position: absolute;
left: 3px;
top: 50%;
bottom: 0;
width: 14px;
border-left: 2px solid #4b5563;
border-top: 2px solid #4b5563;
}
/* 矢じり三角 */
.arrow-target > td.col-arrow::after {
content: '';
position: absolute;
top: 50%;
right: 1px;
transform: translateY(-50%);
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 7px solid #4b5563;
}
border-collapse テーブルでの overflow 問題
border-collapse: collapseを使ったテーブルでは、セルにoverflow: visibleを指定しても効かない。疑似要素がセルの境界で切れてしまう。
対処: 矢印列の幅を当初10pxから20pxに広げ、三角形の矢じりが列幅内に収まるようにした。
.col-arrow {
width: 20px;
min-width: 20px;
max-width: 20px;
padding: 0 !important;
position: sticky;
left: 0;
z-index: 1;
background: #fff;
border-right: none;
}
sticky位置のずらし
col-arrowを先頭に追加したことで、既存のsticky列(col-section, col-name)のleft値を調整する必要があった。
/* col-section: left: 0 → left: 20px */
.col-section {
position: sticky;
left: 20px;
z-index: 1;
}
/* col-name: left: 2.5rem → left: calc(20px + 2.5rem) */
.col-name {
position: sticky;
left: calc(20px + 2.5rem);
z-index: 1;
}
2. 個別財務諸表の多列テーブル化
変更前: T勘定式(左右分割)
以前はIndividualFsTable.vueでT勘定のように借方・貸方を左右に分割して表示していた。見た目は教科書的だが、複数会社の比較がしづらかった。
変更後: 縦並び多列テーブル
IndividualFsColumnarTable.vueを新規作成し、P社・A社(さらにデータセットによってはS社も)を横に並べる多列テーブルに変更した。
<template>
<div class="columnar-fs-wrapper">
<table class="columnar-fs-table">
<thead>
<!-- 1行目: 会社名 -->
<tr class="header-company">
<th class="col-label" :rowspan="hasSublabels ? 3 : 2">勘定科目</th>
<th
v-for="(group, gi) in companyGroups(data)"
:key="gi"
:colspan="group.span"
class="col-company"
>
{{ group.name }}
</th>
</tr>
<!-- 2行目: 補足(支配獲得時 等) -->
<tr v-if="hasSublabels" class="header-sublabel">
<th v-for="(col, ci) in data.columns" :key="ci" class="col-sublabel">
{{ col.periodSublabel ?? '' }}
</th>
</tr>
<!-- 3行目: 日付 -->
<tr class="header-period">
<th v-for="(col, ci) in data.columns" :key="ci" class="col-period">
{{ col.periodLabel }}
</th>
</tr>
</thead>
<!-- ... -->
</table>
</div>
</template>
ヘッダー3行構成
| 行 | 内容 | 例 |
|---|---|---|
| 1行目 | 会社名(colspan) | P社 / S社 |
| 2行目 | 補足(periodSublabel) | 支配獲得時 |
| 3行目 | 日付(periodLabel) | X3年3月31日 |
2行目の補足がないデータセットでは自動的にスキップされる(hasSublabelsのcomputed判定)。
columnNotes対応
S社の土地に時価注記がある場合など、特定のセルに括弧書きの注記を表示できる。
<td class="col-value">
<span v-if="val != null">{{ formatNumber(val) }}</span>
<span v-if="row.columnNotes?.[ci]" class="column-note">
({{ row.columnNotes[ci] }})
</span>
</td>
データ側ではIndividualFsColumnarRow型にcolumnNotesプロパティを持たせた。
export interface IndividualFsColumnarRow {
section: 'bs' | 'pl' | 'ss'
label: string
values: (number | null)[]
columnNotes?: (string | undefined)[]
isSectionHeader?: boolean
isSummary?: boolean
isSubtotal?: boolean
}
前提条件テキストの統合
以前は前提条件カードに「前提条件2: A社の個別B/S」のようなテキストを別セクションとして表示していたが、多列テーブルのヘッダーに会社名・期間・補足を含めたことで、その情報をテーブル自体に統合できた。前提条件セクションからは該当テキストを削除した。
3. データバグ修正
バグ1: 資本剰余金の期首残高
症状: 2年目データセット(worksheet-data-year2.ts)のS/Sテーブルで、資本剰余金の期首残高が0になっていた。
原因: csBeginの計算をcsEnd - csChangeで逆算していたが、当期変動額が0のためcsBegin = csEnd = 0と誤って算出されていた。実際にはA社の資本剰余金は70,000、P社は200,000。
発見方法: テキスト画像全5ページとの照合で発覚。
修正: beginRetainedと同様に、直接参照する方式に変更。
// Before: 逆算方式(バグ)
const csBegin = csEnd - csChange // csChange = 0 → csBegin = 0(誤り)
// After: 直接参照
const csBegin = csEnd - csChange // csEnd が正しい値を持つようになった
実際にはcapitalSurplusVals(cv('525'))の値を正しく使うように修正した。baseValues['525']を{ p: 200000, a: 70000 }と定義し、連結修正仕訳での消去(-70,000)も反映させた。
バグ2: 2年目の未実現利益仕訳でP/L側が欠落
症状: 修正後合算列でB/Sが5,000ズレていた(資産合計と負債純資産合計が一致しない)。
原因: 開始仕訳の未実現利益(aje-open-unrealized)で、前期の未実現利益実現仕訳のP/L側(売上原価 -5,000、コード620)が記載されていなかった。
B/S側だけ処理すると、利益剰余金が当期純利益経由で変動する分が反映されず、貸借がズレる。
修正:
// Before: P/L側が欠落
'aje-open-unrealized': {
'520': -5000, // 利益剰余金期首残高
'130': 5000, // 棚卸資産(前期分は実現済みだが符号が逆)
}
// After: P/L側を追加
'aje-open-unrealized': {
'520': 0, // B/S内で完結(直接-5000 + NI+5000 = 0)
'620': -5000, // 売上原価(前期実現のP/L側)
}
開始仕訳では前期分の未実現利益が実現しているため、(借)利益剰余金期首残高 / (貸)売上原価という仕訳になる。P/Lの売上原価がマイナスになることで当期純利益が+5,000増加し、利益剰余金は「期首-5,000 + NI+5,000 = 0」とB/S内で完結する。
B/S貸借バランステストの改善
以前はaccountRowsを個別にループして資産・負債の各勘定科目を合計する方式でテストしていたが、summary行(isSummary: trueの「資産合計」行と「負債純資産合計」行)を直接比較する方式に変更した。
describe('連結修正仕訳: 各調整列のB/S貸借バランス', () => {
const totalAssetsRow = accountRowsY2.find(
r => r.isSummary && r.name === '資産合計'
)!
const totalLiabEquityRow = accountRowsY2.find(
r => r.isSummary && r.name === '負債純資産合計'
)!
const adjustColumns = [
'aje-open-capital', 'aje-open-goodwill', 'aje-open-unrealized',
'aje-capital', 'aje-goodwill', 'aje-interco', 'aje-unrealized',
'rge-reclass',
]
for (const colId of adjustColumns) {
it(`${colId}: 資産合計 = 負債純資産合計`, () => {
expect(totalAssetsRow.values[colId]).toBe(totalLiabEquityRow.values[colId])
})
}
})
この方式は4データセット(1年目、2年目、追加取得、段階取得)全てに適用した。summary行を使うことで、データ構造が変わっても行の追加・削除にテストが影響されにくくなった。
4. セクションヘッダースタイルの改善
変更前
セクションヘッダー(「連結B/S」「連結P/L」)の背景が薄いグレー(#f3f4f6)で、テーブル内の通常行と区別がつきにくかった。
変更後
濃いグレー(#374151)+ 白文字に変更して、セクションの区切りを明確にした。
.section-label {
font-weight: 700;
font-size: 0.75rem;
color: #ffffff;
background: #374151;
padding: 0.4rem 0.5rem;
position: sticky;
left: 20px;
z-index: 1;
}
.section-empty {
background: #374151;
border-left-color: #4b5563;
}
tr背景 → 個別セル背景への変更
当初は<tr class="section-header">にbackgroundを設定していたが、矢印列(col-arrow)とBS/PL区分列(col-section)は透明にしたかった。<tr>に背景を設定すると、個別セルにbackground: transparentを指定しても<tr>の背景が透けて見えてしまう。
解決策: <tr>のbackgroundをnoneにして、各<td>に個別に背景色を設定した。
/* trは背景なし */
.section-header {
background: none;
}
/* 矢印列・区分列は透明 */
.section-header td.col-arrow,
.section-header td.col-section {
background: transparent;
}
/* 勘定科目名のセルに背景色 */
.section-header .col-name {
background: #374151;
}
colspan分割
以前は勘定科目名セルにcolspan="2"を設定して区分列とまとめていたが、col-sectionを独立した空セルとして分離した。これにより、区分列を透明に保ちつつ、勘定科目名セルだけに濃い背景色を適用できるようになった。
<tr class="section-header">
<td class="col-arrow"></td>
<td class="col-section"></td> <!-- 独立空セル -->
<td class="section-label">連結B/S</td> <!-- 背景色あり -->
<td v-for="col in sheet.columns" class="section-empty"></td>
</tr>
まとめ
| 項目 | 対応 |
|---|---|
| 矢印線 | col-arrow専用列 + CSS疑似要素で描画 |
| 多列テーブル | IndividualFsColumnarTable.vue 新規作成 |
| 資本剰余金バグ | 期首残高の計算方式を直接参照に修正 |
| 未実現利益バグ | 開始仕訳のP/L側(売上原価-5,000)を追加 |
| テスト改善 | summary行ベースのB/S貸借バランス検証に統一 |
| セクションヘッダー | 濃いグレー+白文字、tr→個別セル背景 |
border-collapse: collapseテーブルでoverflow: visibleが効かない制約は地味にハマるポイントだった。テーブル内で装飾的な要素を追加する場合は、専用列を設けて列幅内に収める方が確実。