• #Vue.js
  • #SVG
  • #UI/UX
  • #コンポーネント設計
  • #連結会計
開発eurekapuメモ

概要

連結会計アプリの連結精算表ページにおいて、以下の4つのUIコンポーネント改善を実施した。

  1. 売却パターン図の追加 - PrerequisitesCard.vueに6パターンの売却図を追加
  2. パターン図のSVGサイズ調整 - viewBox幅拡大で右端ラベルの見切れを修正
  3. マイナス値表示の「▲」形式対応 - formatNumber関数を改修し7コンポーネントに適用
  4. 合計仕訳ブロックのスタイル調整 - JournalCategoryTable.vueのタイトル・余白を改善

1. 売却パターン図の追加

背景

連結会計の資本連結では、株式の「追加取得」と「売却」で異なる会計処理が必要になる。既存のPrerequisitesCard.vueには追加取得の5パターンのみが実装されていたため、売却の6パターンを追加した。

パターン定義

追加取得(既存):

// 追加取得の5パターン定義(図表4-1準拠)
const acquisitionPatterns = [
  { num: 1, startPercent: 70, endPercent: 80, label: '' },
  { num: 2, startPercent: 5, endPercent: 80, label: '' },
  { num: 3, startPercent: 30, endPercent: 80, label: '' },
  { num: 4, startPercent: 25, endPercent: 40, label: '' },
  { num: 5, startPercent: 5, endPercent: 25, label: '' },
]

const acquisitionPatternNames = [
  '① 子会社株式を追加取得した場合',
  '② 子会社・関連会社以外から子会社とする場合',
  '③ 関連会社から子会社とする場合',
  '④ 関連会社株式を追加取得した場合',
  '⑤ 子会社・関連会社以外から関連会社とする場合',
]

売却(新規追加):

// 売却の6パターン定義(第5章準拠)
const salePatterns = [
  { num: 1, startPercent: 80, endPercent: 70, label: '' },
  { num: 2, startPercent: 80, endPercent: 30, label: '' },
  { num: 3, startPercent: 80, endPercent: 10, label: '' },
  { num: 4, startPercent: 80, endPercent: 0, label: '' },
  { num: 5, startPercent: 70, endPercent: 30, label: '' },
  { num: 6, startPercent: 80, endPercent: 30, label: '' },
]

const salePatternNames = [
  '① 支配関係が継続する場合',
  '② 支配を喪失し関連会社になった場合',
  '③ 支配を喪失し関連会社にも該当しなくなった場合',
  '④ 子会社株式の全部を売却した場合',
  '⑤ 一部売却後に一部売却を行い関連会社になった場合',
  '⑥ 追加取得後に一部売却を行い関連会社になった場合',
]

売却パターン図のSVG実装

売却パターンでは、追加取得と異なり下向き矢印で持分の減少を表現する。

<!-- 売却パターン: 売却前のバー(上部)&売却後の点線バー -->
<template v-if="patternType === 'sale'">
  <!-- 売却後のバー(残存持分) -->
  <rect
    v-if="p.endPercent > 0"
    :x="50 + i * 42"
    :y="150 - (p.endPercent / 100) * 136"
    width="22"
    :height="(p.endPercent / 100) * 136"
    :class="prerequisites.acquisitionPattern!.patternNumber === p.num
      ? 'bar-before-active' : 'bar-before'"
    rx="2"
  />
  <!-- 売却前の持分(点線枠で示す) -->
  <rect
    :x="50 + i * 42"
    :y="150 - (p.startPercent / 100) * 136"
    width="22"
    :height="((p.startPercent - p.endPercent) / 100) * 136"
    fill="none"
    :stroke="prerequisites.acquisitionPattern!.patternNumber === p.num
      ? '#ef4444' : '#d1d5db'"
    stroke-width="1.5"
    stroke-dasharray="3 2"
    rx="2"
  />
  <!-- 下向き矢印(売却による減少) -->
  <line
    :x1="61 + i * 42"
    :y1="150 - (p.startPercent / 100) * 136 + 4"
    :x2="61 + i * 42"
    :y2="150 - (p.endPercent / 100) * 136 - 5"
    :class="prerequisites.acquisitionPattern!.patternNumber === p.num
      ? 'pattern-arrow-sale-active' : 'pattern-arrow-sale'"
    stroke-width="2"
  />
  <polygon
    :points="`${57 + i * 42},${150 - (p.endPercent / 100) * 136 - 5}
              ${61 + i * 42},${150 - (p.endPercent / 100) * 136 + 2}
              ${65 + i * 42},${150 - (p.endPercent / 100) * 136 - 5}`"
    :class="prerequisites.acquisitionPattern!.patternNumber === p.num
      ? 'pattern-arrowhead-sale-active' : 'pattern-arrowhead-sale'"
  />
</template>

パターンタイプの自動判定

データファイルのacquisitionPattern.patternTypeプロパティで追加取得/売却を判定:

const patternType = computed(() =>
  props.prerequisites.acquisitionPattern?.patternType ?? 'acquisition'
)
const allPatterns = computed(() =>
  patternType.value === 'sale' ? salePatterns : acquisitionPatterns
)
const patternNames = computed(() =>
  patternType.value === 'sale' ? salePatternNames : acquisitionPatternNames
)
const patternTitle = computed(() =>
  patternType.value === 'sale' ? '子会社株式売却のパターン' : '追加取得のパターン'
)

売却パターン用のCSS

/* 矢印: 非アクティブ(売却) */
.pattern-arrow-sale {
  stroke: #d1d5db;
}

.pattern-arrowhead-sale {
  fill: #d1d5db;
}

/* 矢印: アクティブ(売却) - 赤色で売却を強調 */
.pattern-arrow-sale-active {
  stroke: #ef4444;
}

.pattern-arrowhead-sale-active {
  fill: #ef4444;
}

2. パターン図のSVGサイズ調整

問題

パターン図の右端にある「子会社」「関連会社」のラベルが見切れていた。

原因

SVGのviewBox幅が不足していた。

修正内容

<!-- Before -->
<svg viewBox="0 0 300 190" class="diagram-svg pattern-svg">

<!-- After: パターンタイプに応じてviewBox幅を調整 -->
<svg :viewBox="patternType === 'sale' ? '0 0 370 190' : '0 0 340 190'"
     class="diagram-svg pattern-svg">
パターン修正前修正後
追加取得300px340px
売却-370px

売却パターンは6パターンあるため、追加取得(5パターン)より幅が必要。

グリッド線とラベル位置の調整

<!-- グリッド線の終端位置をpatternTypeで調整 -->
<line x1="40" y1="14"
      :x2="patternType === 'sale' ? 300 : 270"
      y2="14" stroke="#e5e7eb" stroke-width="1" />

<!-- 閾値ラベルの位置も調整 -->
<text :x="patternType === 'sale' ? 308 : 278" y="82"
      class="threshold-label threshold-sub">子会社</text>
<text :x="patternType === 'sale' ? 308 : 278" y="120"
      class="threshold-label threshold-assoc">関連会社</text>

max-widthの緩和

/* パネル幅いっぱいに広がるよう制限を緩和 */
.pattern-svg {
  max-width: 100%;
}

3. マイナス値表示の「▲」形式対応

背景

会計帳票では、マイナス値を「-100」ではなく「▲100」と表示するのが一般的。この形式に対応するため、共通のformatNumber関数を改修した。

formatNumber関数の実装

// apps/web/app/components/consolidated-worksheet/data/types.ts

export const formatNumber = (n: number): string =>
  n === 0 ? '-' : n < 0
    ? `<span class="neg-mark">▲</span>${(-n).toLocaleString('ja-JP')}`
    : n.toLocaleString('ja-JP')

設計ポイント

  1. ゼロ値: -(ハイフン)で表示
  2. 負の値: マーク + 絶対値(カンマ区切り)
  3. 正の値: そのままカンマ区切り
  4. HTMLタグを返す: v-htmlディレクティブで描画

スタイル定義

.td-amount :deep(.neg-mark) {
  font-size: 0.75em;
  vertical-align: baseline;
  margin-right: 1px;
}

:deep()を使用してスコープ付きCSS内でも.neg-markクラスにスタイルを適用。

更新したコンポーネント(7ファイル)

コンポーネント用途
JournalCategoryTable.vue連結修正仕訳の表示
WorksheetTable.vue連結精算表の表示
EquityCalcTable.vue持分計算表の表示
IndividualFinancialStatements.vue個別財務諸表
PLStatementTable.vue損益計算書
BSStatementTable.vue貸借対照表
consolidation-simulator.vue連結シミュレーター

4. 合計仕訳ブロックのスタイル調整

背景

JournalCategoryTable.vueでは、同一のtargetColumnに複数の仕訳がある場合、自動的に「合計仕訳」を計算して表示する。この合計仕訳ブロックが個別仕訳と見分けがつきにくかったため、スタイルを改善した。

合計仕訳の自動計算ロジック

// targetColumn ごとにエントリをグループ化し、2件以上あるグループの合計を計算
const columnSummaries = computed<Map<string, ColumnSummary>>(() => {
  const groups = new Map<string, EntryView[]>()
  for (const ev of entries.value) {
    const key = ev.entry.targetColumn
    const arr = groups.get(key)
    if (arr) arr.push(ev)
    else groups.set(key, [ev])
  }

  const result = new Map<string, ColumnSummary>()

  for (const [colId, evs] of groups) {
    if (evs.length < 2) continue

    // 勘定科目ごとに借方・貸方を合算
    const accountMap = new Map<string, { debit: number; credit: number }>()
    for (const ev of evs) {
      for (const line of ev.entry.debit) {
        const acc = accountMap.get(line.account) ?? { debit: 0, credit: 0 }
        acc.debit += line.amount
        accountMap.set(line.account, acc)
      }
      for (const line of ev.entry.credit) {
        const acc = accountMap.get(line.account) ?? { debit: 0, credit: 0 }
        acc.credit += line.amount
        accountMap.set(line.account, acc)
      }
    }

    // 純額計算: 借方 > 貸方 → 借方側、貸方 > 借方 → 貸方側、差額0 → 省略
    const debitLines: SummaryAccountLine[] = []
    const creditLines: SummaryAccountLine[] = []
    for (const [account, { debit, credit }] of accountMap) {
      const net = debit - credit
      if (net === 0) continue
      if (net > 0) debitLines.push({ account, amount: net })
      else creditLines.push({ account, amount: -net })
    }

    // ... 結果をMapに格納
  }

  return result
})

スタイル改善のポイント

/* 合計仕訳のタイトル行 */
.summary-title {
  padding: 1.5rem 0.5rem 0.5rem 0.5rem;  /* 上部に大きな余白 */
  font-weight: 700;
  font-size: 1.15rem;  /* 個別仕訳(0.8rem)より大きく */
  color: #1e40af;
  background: #fff;
  border-top: 3px solid #3b82f6;  /* 上部に青い境界線 */
}

/* 合計仕訳のテーブル本体 */
.summary-entry-group {
  background: #f8fafc;  /* 薄いグレー背景で区別 */
}

/* クリック可能な行のスタイル */
.summary-row-clickable {
  cursor: pointer;
}

.summary-row-clickable:hover td {
  background: #fef3c7;  /* ホバー時に黄色 */
}

インタラクション: 合計仕訳と個別仕訳のリンク

合計仕訳の勘定科目行をクリックすると、その科目を含む個別仕訳がハイライトされる:

const highlightedAccount = ref<{ targetColumn: string; account: string } | null>(null)

const toggleHighlight = (targetColumn: string, account: string) => {
  if (highlightedAccount.value?.targetColumn === targetColumn
      && highlightedAccount.value?.account === account) {
    highlightedAccount.value = null
  } else {
    highlightedAccount.value = { targetColumn, account }
  }
}

// 個別仕訳の行がハイライト対象かを判定
const isHighlighted = (ev: EntryView, account: string): boolean => {
  if (!highlightedAccount.value) return false
  return highlightedAccount.value.targetColumn === ev.entry.targetColumn
    && highlightedAccount.value.account === account
}

まとめ

今回の改善により、連結会計アプリのUIが以下の点で向上した:

  1. 視覚的な明確さ: 売却パターン図で持分変動の方向(下向き矢印)が直感的に理解できる
  2. 情報の完全性: SVGサイズ調整でラベルの見切れを解消
  3. 会計帳票の慣習への準拠: マイナス値の「▲」表示で専門家にも違和感のない表示
  4. UXの改善: 合計仕訳と個別仕訳の関係がインタラクティブに確認可能

変更ファイル一覧

ファイル変更内容
PrerequisitesCard.vue売却パターン6種追加、SVGサイズ調整
data/types.tsformatNumber関数改修
JournalCategoryTable.vue合計仕訳スタイル改善
他6コンポーネントformatNumber適用