• #連結会計
  • #Vue
  • #CSS
  • #テーブルUI
  • #デバッグ
開発mdx-playgroundメモ

連結精算表の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 が正しい値を持つようになった

実際にはcapitalSurplusValscv('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が効かない制約は地味にハマるポイントだった。テーブル内で装飾的な要素を追加する場合は、専用列を設けて列幅内に収める方が確実。