家計簿アプリを作る #47:年間サマリーページ

家計簿アプリ作成シリーズの第47回です。これまでは月ごとの収支や推移は見られましたが、「1年を通してどうだったか」を一覧できる画面が無かったので、年単位の収支サマリーページを作ります。

実装方針

/summary ページを新規作成し、年を選択できる Select と、選択した年の月別収支テーブル・年間合計を表示します。既存の収支一覧ページの「月次推移」の集計ロジック(allTransactions を月ごとに filter/reduce)と考え方は同じで、集計の粒度を「年」に変える形です。

numberstring の比較エラー

年の一覧を作る際、最初は date.getFullYear() の戻り値(number)をそのまま使っていました。

// ❌ numberの配列になってしまう
const uniqueYears = Array.from(
  new Set(allTransactions.map((t) => new Date(t.date).getFullYear()).sort((a, b) => b - a))
);

一方、クエリパラメータから取得する selectedYearstring なので、+page.svelte 側で year === data.selectedYear と比較したときに

This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.

という型エラーになりました。既存の months(月の一覧)が文字列で統一されているのに合わせて、uniqueYears も文字列に変換して解消しました。

// ✅ 文字列に統一する
const uniqueYears = Array.from(
  new Set(allTransactions.map((t) => String(new Date(t.date).getFullYear())).sort((a, b) => (a > b ? -1 : 1)))
);

月別テーブルの位置づけ

年間サマリーに何を表示するか検討する中で、「月別の情報をテーブルで表示するのは既存の収支一覧ページのテーブルと同じでは?」という点を整理しました。

  • 収支一覧ページのテーブル: 選択したの中の、個々の取引を1件ずつ並べたもの
  • 年間サマリーのテーブル: 選択したの中で、月ごとに集計した合計値を1行ずつ並べたもの(最大12行)

粒度が違うので、両方あっても重複にはなりません。列は「年月・収入合計・支出合計・残高」の4列にしました。

const monthlyTransactions = Array.from(
  new Set(yearTransactions.map((t) => {
    const date = new Date(t.date);
    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
  }))
).map(month => {
  const monthTransactions = yearTransactions.filter(t => t.date.startsWith(month));
  const monthIncome = monthTransactions.filter(t => t.type === 'income').reduce((sum, t) => sum + t.amount, 0);
  const monthExpense = monthTransactions.filter(t => t.type === 'expense').reduce((sum, t) => sum + t.amount, 0);
  return {
    month,
    totalMonthlyIncome: monthIncome,
    totalMonthlyExpense: monthExpense,
    totalMonthlyBalance: monthIncome - monthExpense,
  };
});

まとめ

ポイント 内容
年の一覧の型 既存のmonthsに合わせて文字列で統一する
月別テーブルの意義 収支一覧の取引明細とは粒度が異なるので重複しない
テーブルの列構成 年月・収入合計・支出合計・残高の4列で十分
← トップページに戻る