初回コミット
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.0.1",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "家計簿アプリ",
|
||||||
|
"runtimeExecutable": "npm",
|
||||||
|
"runtimeArgs": ["run", "dev"],
|
||||||
|
"port": 5173
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(node -e \":*)",
|
||||||
|
"Bash(npm create:*)",
|
||||||
|
"Bash(echo {})",
|
||||||
|
"Bash(npm init:*)",
|
||||||
|
"Bash(npm install:*)",
|
||||||
|
"Bash(npm run:*)",
|
||||||
|
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:5173/)",
|
||||||
|
"Bash(start /B cmd /c \"npx vite --port 5173 > C:/Users/nack/AppData/Local/Temp/vite-dev.log 2>&1\")",
|
||||||
|
"Bash(npm list:*)",
|
||||||
|
"Bash(file \"E:/YOU/Programs/ClaudeCode/家計簿アプリ/MoneyForwardエクスポート/\"*.csv)",
|
||||||
|
"Bash(npx tsc:*)",
|
||||||
|
"mcp__Claude_Preview__preview_start",
|
||||||
|
"Bash(npm uninstall:*)",
|
||||||
|
"mcp__playwright__browser_take_screenshot",
|
||||||
|
"mcp__playwright__browser_snapshot",
|
||||||
|
"mcp__playwright__browser_select_option",
|
||||||
|
"mcp__playwright__browser_evaluate",
|
||||||
|
"mcp__playwright__browser_console_messages"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.local
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[ 379ms] [WARNING] The width(150) and height(180) are both fixed numbers,
|
||||||
|
maybe you don't need to use a ResponsiveContainer. @ http://localhost:5173/node_modules/.vite/deps/recharts.js?v=3558bc95:6196
|
||||||
|
[ 379ms] [WARNING] The width(150) and height(180) are both fixed numbers,
|
||||||
|
maybe you don't need to use a ResponsiveContainer. @ http://localhost:5173/node_modules/.vite/deps/recharts.js?v=3558bc95:6196
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[ 4714ms] [WARNING] The width(150) and height(180) are both fixed numbers,
|
||||||
|
maybe you don't need to use a ResponsiveContainer. @ http://localhost:5173/node_modules/.vite/deps/recharts.js?v=3558bc95:6196
|
||||||
|
[ 4715ms] [WARNING] The width(150) and height(180) are both fixed numbers,
|
||||||
|
maybe you don't need to use a ResponsiveContainer. @ http://localhost:5173/node_modules/.vite/deps/recharts.js?v=3558bc95:6196
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
[ 990576ms] TypeError: Cannot read properties of undefined (reading 'includes')
|
||||||
|
at http://localhost:5173/src/utils/accounts.ts:166:52
|
||||||
|
at Array.find (<anonymous>)
|
||||||
|
at findAccountByInstitution (http://localhost:5173/src/utils/accounts.ts:166:28)
|
||||||
|
at convertToTransactions (http://localhost:5173/src/utils/csvParser.ts?t=1774798782693:83:28)
|
||||||
|
at reapplyMappings (http://localhost:5173/src/store/index.ts?t=1774798782693:101:52)
|
||||||
|
at onClick (http://localhost:5173/src/components/ImportPage.tsx?t=1774799081466:361:29)
|
||||||
|
at HTMLUnknownElement.callCallback2 (http://localhost:5173/node_modules/.vite/deps/chunk-PJEEZAML.js?v=3558bc95:3674:22)
|
||||||
|
at Object.invokeGuardedCallbackDev (http://localhost:5173/node_modules/.vite/deps/chunk-PJEEZAML.js?v=3558bc95:3699:24)
|
||||||
|
at invokeGuardedCallback (http://localhost:5173/node_modules/.vite/deps/chunk-PJEEZAML.js?v=3558bc95:3733:39)
|
||||||
|
at invokeGuardedCallbackAndCatchFirstError (http://localhost:5173/node_modules/.vite/deps/chunk-PJEEZAML.js?v=3558bc95:3736:33)
|
||||||
|
[ 990577ms] TypeError: Cannot read properties of undefined (reading 'includes')
|
||||||
|
at http://localhost:5173/src/utils/accounts.ts:166:52
|
||||||
|
at Array.find (<anonymous>)
|
||||||
|
at findAccountByInstitution (http://localhost:5173/src/utils/accounts.ts:166:28)
|
||||||
|
at convertToTransactions (http://localhost:5173/src/utils/csvParser.ts?t=1774798782693:83:28)
|
||||||
|
at reapplyMappings (http://localhost:5173/src/store/index.ts?t=1774798782693:101:52)
|
||||||
|
at onClick (http://localhost:5173/src/components/ImportPage.tsx?t=1774799081466:361:29)
|
||||||
|
at HTMLUnknownElement.callCallback2 (http://localhost:5173/node_modules/.vite/deps/chunk-PJEEZAML.js?v=3558bc95:3674:22)
|
||||||
|
at Object.invokeGuardedCallbackDev (http://localhost:5173/node_modules/.vite/deps/chunk-PJEEZAML.js?v=3558bc95:3699:24)
|
||||||
|
at invokeGuardedCallback (http://localhost:5173/node_modules/.vite/deps/chunk-PJEEZAML.js?v=3558bc95:3733:39)
|
||||||
|
at invokeGuardedCallbackAndCatchFirstError (http://localhost:5173/node_modules/.vite/deps/chunk-PJEEZAML.js?v=3558bc95:3736:33)
|
||||||
|
[ 1530690ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1534067ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1537424ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1540802ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1544160ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1547540ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1550921ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1554299ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1557675ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1561029ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
|
[ 1564384ms] [ERROR] Failed to load resource: net::ERR_CONNECTION_REFUSED @ http://localhost:5173/:0
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# 家計簿アプリ
|
||||||
|
|
||||||
|
MoneyForward のCSVを取り込んで複式簿記で管理する個人用Webアプリ。
|
||||||
|
|
||||||
|
## 起動方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# → http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技術スタック
|
||||||
|
|
||||||
|
- React 18 + TypeScript + Vite
|
||||||
|
- Tailwind CSS(ダークテーマ、slate系カラー)
|
||||||
|
- Zustand(状態管理、localStorageにpersist)
|
||||||
|
- PapaParse(Shift-JIS CSV読み込み)
|
||||||
|
- Recharts(棒グラフ・ドーナツ円グラフ)
|
||||||
|
|
||||||
|
## 主要ファイル
|
||||||
|
|
||||||
|
| ファイル | 役割 |
|
||||||
|
|---|---|
|
||||||
|
| `src/types/index.ts` | 型定義(Account, Transaction, JournalEntry等)|
|
||||||
|
| `src/utils/accounts.ts` | 勘定科目マスタ・カテゴリ/金融機関マッピングのデフォルト値 |
|
||||||
|
| `src/utils/csvParser.ts` | MoneyForward CSVパース → 複式仕訳変換 |
|
||||||
|
| `src/utils/bookkeeping.ts` | 残高計算・P/L・B/S・元帳構築ロジック |
|
||||||
|
| `src/store/index.ts` | Zustandストア(全データ管理) |
|
||||||
|
| `src/components/` | 各画面コンポーネント |
|
||||||
|
|
||||||
|
## 画面構成
|
||||||
|
|
||||||
|
- **ダッシュボード**: 月次収支サマリ、棒グラフ、支出内訳円グラフ、費目別ランキング
|
||||||
|
- **CSVインポート**: Shift-JIS CSV ドロップ→仕訳変換(重複自動スキップ)
|
||||||
|
- **仕訳帳**: 全仕訳一覧(列幅リサイズ対応)
|
||||||
|
- **総勘定元帳**: 勘定科目別の借方/貸方/残高明細
|
||||||
|
- **バランスシート**: 貸借対照表(B/S)と損益計算書(P/L)タブ
|
||||||
|
- **設定・マッピング**: MFカテゴリ→勘定科目、金融機関→資産勘定のマッピング管理
|
||||||
|
|
||||||
|
## 重要な設計上の注意点
|
||||||
|
|
||||||
|
### `_skip_` accountId
|
||||||
|
ATM引き出し・Suicaチャージ・口座間振替など「資産間移動」はカテゴリマッピングで `_skip_` を指定すると仕訳生成をスキップする。ただし `天引き貯金` と `投資用振り込み` は資産移動だが**支出として計上**する方針。
|
||||||
|
|
||||||
|
### 勘定科目タイプ
|
||||||
|
`asset` / `liability` / `equity` / `income` / `expense` の5種。`normalBalance` が `debit` なら借方残高科目、`credit` なら貸方残高科目。
|
||||||
|
|
||||||
|
### 選択月の共有
|
||||||
|
`selectedMonth` はZustandストアで全ページ共有。ページ切り替えで月選択がリセットされないようにしている。
|
||||||
|
|
||||||
|
### データの保存場所
|
||||||
|
全データはブラウザの **localStorage** に保存(Zustand persist)。ブラウザの閲覧データを削除すると消える。コードからは参照不可。
|
||||||
|
|
||||||
|
## よくある改善作業
|
||||||
|
|
||||||
|
- **未分類カテゴリの追加**: `src/utils/accounts.ts` の `DEFAULT_CATEGORY_MAPPINGS` または `DEFAULT_INSTITUTION_MAPPINGS` に追記し、「設定」画面で「デフォルトを再適用」ボタンを押してもらう
|
||||||
|
- **勘定科目の追加**: `src/utils/accounts.ts` の `DEFAULT_ACCOUNTS` に追記(`id` はユニークな文字列、`order` で表示順を制御)
|
||||||
|
- **新しいMF金融機関の追加**: `DEFAULT_INSTITUTION_MAPPINGS` に追記
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"計算対象","日付","内容","金額(円)","保有金融機関","大項目","中項目","メモ","振替","ID"
|
||||||
|
"0","2026/01/22","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","8a1TpgVYmQFGU_VqfMnKCg5pmD1Ki2LqEK83Jy23R5U"
|
||||||
|
"0","2026/01/22","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","AbeBErrZ7vJyWTyivtsCfHVptg_v19qZLYDOmuZyhGc"
|
||||||
|
"0","2026/01/21","オート 小 新宿","3000","モバイルSuica","未分類","未分類","","1","XhB48pP_c9efAb4SpeRedoHwCszez26b09dOkI4ItAQ"
|
||||||
|
"0","2026/01/21","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","b60pjQs4orDA9KCmQiwUum7pO0JpENz8UR7j78n1eKY"
|
||||||
|
"0","2026/01/21","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","WlGk3uCKXxoZhpW1mAXNCKodakZY8L1-ot14EX_ll3U"
|
||||||
|
"0","2026/01/21","振込*ナカシマ ユウジ","30000","住信SBIネット銀行","収入","自分の口座から移動","","1","1oruHj8JeHXIKgjbp0FqitQEeJiGN_KhZu3gkmnFmdE"
|
||||||
|
"0","2026/01/21","小田急電鉄 新宿駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","amuR0nMgJn185qFONBMOzW_rUHJ19QLLp9BOXqyILqw"
|
||||||
|
"0","2026/01/21","住信SBIネット銀行 イチゴ支店 普通預金 3803507 ナカシマ ユウジ(依頼人名:ナカシマ ユウジ 振込予定日:2026年01月21日 管理番号:20260121-98294806)","-30000","楽天銀行","未分類","未分類","","1","PTbbaktwY0BdWUT5RqEd4FJq7MTnf8f_bS4XtRMUJ_U"
|
||||||
|
"1","2026/01/21","振込*ナカシマ スズコ","-20000","住信SBIネット銀行","その他","仕送り","お母さんの仕送り","0","5sWNgoxA-xOf4yJ_oqY0f4jh6wwSbsfSYo2m6_pn5Yk"
|
||||||
|
"1","2026/01/21","普通 円 すずこうざ","15000","住信SBIネット銀行","収入","その他入金","仕送り引き落し","1","ki2EvSA7oe9B4b-3o_FMeXmrARJsqxePdFXTgacXTFo"
|
||||||
|
"0","2026/01/20","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","iGcQg4DoH3vegEzrbUqnRIG7qWbhwnk4b-IAGGMozYU"
|
||||||
|
"0","2026/01/20","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","qt-kgm9RDv5SGKnHqJhqvc6RsvJr39e9Tqj_JCkdUjo"
|
||||||
|
"1","2026/01/20","クリエイトエスデイ―","-110","三井住友カード (VpassID)","食費","食費","パンとか","0","1jFpYt8NyCO_B52ouvJPCd5xigkdkFPTikjIoAQNyUs"
|
||||||
|
"1","2026/01/20","APPLE COM BILL","-150","三井住友カード (VpassID)","趣味・娯楽","サブスク","iCloud","0","kdKlqAFk3ePkJmOlPO2JyyV3hF_S7MCB8OUlT8-y3Mw"
|
||||||
|
"1","2026/01/19","EAST CDラジカセ EA-CRCD AM/FM対応 CD カセット アズマ 販売: hit-market あなたの欲しいにヒットする","-4990","Amazon.co.jp","特別な支出","家具・家電","お母さんのラジカセ","0","Bd6ER7CtCII-dj-er4wTeCxzKZybuV7W4MSULxEaoPk"
|
||||||
|
"1","2026/01/18","AbemaTV","-1080","三井住友カード (VpassID)","趣味・娯楽","サブスク","abemaTV","0","k1MqAwangepKr1FeD8bwLqcJXKSE8HoI6t0WzII7QNs"
|
||||||
|
"0","2026/01/18","AMAZON.CO.JP","-4990","三井住友カード (VpassID)","日用品","タバコ","","1","dT9_d-xY7fB63m0-Wx19D_MpMHMz52AymBmX5RZoIlM"
|
||||||
|
"1","2026/01/18","サイゼリヤ/NFC","-1000","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","GPhoqtFCkiyer8pAjZjwpO3Sj-oy8THQEN2bzFhXcvk"
|
||||||
|
"1","2026/01/18","地方税","-1","住信SBIネット銀行","税・社会保障","所得税・住民税","利息の税金_地方","0","IihMmo3ETUC9dm7WpWJMoUNUubypFONo9u9TWHQDWpQ"
|
||||||
|
"1","2026/01/18","国税","-4","住信SBIネット銀行","税・社会保障","所得税・住民税","利息の税金_国","0","uQ-vHt403L4NQH5YjoWagn2BobWT4XAlL5Hq70TffgY"
|
||||||
|
"1","2026/01/18","利息","31","住信SBIネット銀行","収入","その他入金","利息","0","j5FSR31UHx_iVoSstMIj_sh4jRANJSniK4SOX7yEgNU"
|
||||||
|
"1","2026/01/18","Amazonポイント","607","Amazon.co.jp","収入","ポイント","Amazonポイント使用","0","MJ6TsZx3qT0aM1QZTwFJbsV0v16STfozgn_DcGzouRI"
|
||||||
|
"1","2026/01/18","【指定第2類医薬品】 禁煙補助薬 ニコチネル ミント 90個 販売: Amazon.co.jp","-3944","Amazon.co.jp","日用品","タバコ","ニコチンガム","0","BJAL5vyEDNZUYaMZTvRBgrlrx1Z2BMDTvIRY1qIv2RQ"
|
||||||
|
"1","2026/01/17","マクドナルド","-150","三井住友カード (VpassID)","食費","カフェ","マクドナルド","0","57tTU74ktrRmuBB0Ew9HSae8MiVyzFk_0aJmmdLa3Ck"
|
||||||
|
"0","2026/01/17","AMAZON.CO.JP","-3337","三井住友カード (VpassID)","日用品","タバコ","","1","bgIeLuEDITMPvb-i-_rcC2mjYM25zt9ZitnlKkObXWU"
|
||||||
|
"0","2026/01/17","Amazon Downloads","-891","三井住友カード (VpassID)","趣味・娯楽","本","","1","47fyndRc0h-bFI8XNpH3VRjJS2_Fti8HuNVF5gsDXTo"
|
||||||
|
"1","2026/01/17","介護未満の父に起きたこと(新潮新書) 販売: Amazon Services International LLC","-891","Amazon.co.jp","趣味・娯楽","本","電子ブック","0","lTe3wdjBGSyUQ-gfpY4ItJ304-GuIXshaVs09mGJ-xM"
|
||||||
|
"1","2026/01/16","ENEOS-SS","-1621","三井住友カード (VpassID)","自動車","ガソリン","ガソリン代","0","FvdSWWqvCNmRE8IeYjZPugPhu8qTDe2iwNB8UA4jOKs"
|
||||||
|
"1","2026/01/16","マクドナルド","-150","三井住友カード (VpassID)","食費","カフェ","マクドナルド","0","WhN8XEIj41hh1TldEUAVdDpG_-wf7N_2k1qe7ubdOLI"
|
||||||
|
"0","2026/01/15","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","cVqwTti5I7oRPWOHrHACUfg0XpYH3sPv2QTCW3xwIss"
|
||||||
|
"0","2026/01/15","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","mbkE_Z5lGeQoB0LTrL9Ue25b4ftPXRiyvQYrOURoYeg"
|
||||||
|
"1","2026/01/15","ソフトバンクM(12月分)","-994","三井住友カード (VpassID)","通信費","携帯電話","linemo","0","CZHSecNedD878glZDHnDVQb6Mv5yzxccHdS6kVMkne0"
|
||||||
|
"1","2026/01/15","ソフトバンクM(12月分)","-5649","三井住友カード (VpassID)","通信費","携帯電話","Y!mobile","0","KenBzkm1fx8F_f7qpPQq4ZPRFyCdZbm9fV8BdKokky4"
|
||||||
|
"1","2026/01/15","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","新宿のドトール","0","FIpVzg0aELD9YhSh-3ShXTWNv-Vr8exjzyvYXdk48XE"
|
||||||
|
"0","2026/01/14","オート 小 新宿","3000","モバイルSuica","未分類","未分類","","1","i8Jg4voU1JBMwShAGpmtcbKxmNqO_nREoC3ULZIEIrQ"
|
||||||
|
"0","2026/01/14","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","lMuPebm603WJOP_0TZyph99xa1JPl3wXItrlS9luPWc"
|
||||||
|
"0","2026/01/14","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","QG7q61OXPmTFOlphtGu-b8BRsTFMPpfMsht1V26iSCA"
|
||||||
|
"1","2026/01/14","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","新宿のドトール","0","9zDVnVaS7zNgCEj87WFdUgOZRo56PDJ00MVcmBe8j-I"
|
||||||
|
"1","2026/01/14","あいおいニッセイ同和損害保険","-1500","三井住友カード (VpassID)","自動車","自動車保険","バイクの保険","0","SMTW5MyF3IS1e6FPr9CZC1UTGzEL3xLdgSrurpWi4Jc"
|
||||||
|
"0","2026/01/14","小田急電鉄 新宿駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","Im7sxgHvyjTn6t0xXy-CYv4rmMdXiV41WW4mX_6YCQs"
|
||||||
|
"0","2026/01/13","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","sa7u4Ccc4nkTs0EpmDDT6b4pOndbd7e-iK4UNoE-RSw"
|
||||||
|
"0","2026/01/13","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","-RN76EdsHDliwef5M0br4cbnogKTsk78ETeesuAGsO8"
|
||||||
|
"1","2026/01/13","UQ mobileご利用料金","-2083","三井住友カード (VpassID)","通信費","携帯電話","UQMobile","0","65vC25hiYaP44AZU_eCrj7on-FhEzdeX6VU9rj6hy_I"
|
||||||
|
"1","2026/01/13","京王モ-ル アネックス/NFC","-330","三井住友カード (VpassID)","食費","カフェ","新宿のドトール","0","37pUZOIom1GNCxnYr3KZP-I9r4Y0ovtMEQTsPC3MGgQ"
|
||||||
|
"1","2026/01/13","SBI証券投信積立サービス","-100000","三井住友カード (VpassID)","その他","投資用振り込み","積み立て投資","0","W9Skjf_rkIdEPWGOzPWztx4nploE-e8q5XAKDoiK1Uc"
|
||||||
|
"1","2026/01/12","DCS・エビナビナウオ―クテン","-330","三井住友カード (VpassID)","食費","カフェ","ドトール","0","4DyZhgceJudJa4fCH7hbPfNM7x416Yar8_gHsV9U3kk"
|
||||||
|
"1","2026/01/12","ファミリーマート","-108","三井住友カード (VpassID)","食費","飲み物","水","0","pdEnHk7xQ3h2uOeAzJPtBVxDPZ-36R6D1bXuDA8Ms-U"
|
||||||
|
"1","2026/01/12","セリア 三和座間東原店","-333","三井住友カード (VpassID)","自動車","バイク関連","洗車用品","0","bmbqRIb7cjsXNrogxc4O5XMNkCi-olK9Cl-iygWRr84"
|
||||||
|
"1","2026/01/12","2りんかんイエローハット","-1100","三井住友カード (VpassID)","自動車","バイク関連","洗車","0","xD7vMDL5SLL9txdSC6hoOs9USZscxfSOlZsvB_MCKrk"
|
||||||
|
"1","2026/01/12","タンタンメン","-750","リアルの個人財布","食費","外食","タンタンメン","0","hSpK_-M2IRD0Y8Noyhl-N0AmKqJEU8K9k_Oj6lLhXUE"
|
||||||
|
"1","2026/01/11","入 入谷 出 橋本","-242","モバイルSuica","交通費","電車","休日移動","0","FwV7XiZbLuCy6E34gJkSPxQRXEdPD0a37Tie6i5jp7c"
|
||||||
|
"1","2026/01/11","入 橋本 出 入谷","-242","モバイルSuica","交通費","電車","休日移動","0","JiikTntkBp9jCqbuQ_3kPoCCrAcvgrLUJTIYpPSrBo8"
|
||||||
|
"1","2026/01/11","楽天モバイル通信料","-1986","楽天カード","通信費","携帯電話","楽天モバイル","0","oexmyFzC5on7FnRy5bD8XgoldTGsRZ8Hvk8kuXMfzWU"
|
||||||
|
"1","2026/01/11","サイゼリヤ/NFC","-650","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","lkcaFpZqB-dFYY-0Qiz_w1LRK4uE4FG6_mHOXvEcwzM"
|
||||||
|
"1","2026/01/11","【指定第2類医薬品】 禁煙補助薬 ニコチネル ミント 90個 販売: Amazon.co.jp","-3944","Amazon.co.jp","日用品","タバコ","ニコチンガム","0","BKJPwrHa07ND_1TWsMNUmX0x-hoZvzWp106N2YfXi-s"
|
||||||
|
"1","2026/01/10","入 相武台前 出 座間","-136","モバイルSuica","交通費","電車","休日移動","0","rOeFTFBEVFqc0RtkoWnch-6kytUJzNFiAeaxpe9JlK0"
|
||||||
|
"0","2026/01/10","AMAZON.CO.JP","-3944","三井住友カード (VpassID)","日用品","タバコ","ニコチンガム_支払い","1","G3oblJJUKrjujTAlizgrz8mdzjIGdcYDGg0IxMBLu2Q"
|
||||||
|
"1","2026/01/10","タイムズカー 202601リヨウリヨウキン","-1100","三井住友カード (VpassID)","自動車","カーシェア","タイムズカー","0","OIXl8wQG2xfVfonygw1qAC1BQevHqbVNaPkTSq2JZA8"
|
||||||
|
"1","2026/01/10","でんき(KDDI)","-12965","三井住友カード (VpassID)","水道・光熱費","電気代","電気代","0","Y_RirtL6QMC_8DHFO6PZ-TE35HXaTlgXL49zEG6Yrag"
|
||||||
|
"1","2026/01/10","UQ mobileご利用料金","-3744","三井住友カード (VpassID)","通信費","携帯電話","UQMobile","0","S6wvR8tNVLLPxu898UwLTI1eX4gzu-ITA6g66GVRBUs"
|
||||||
|
"1","2026/01/10","サイゼリヤ/NFC","-600","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","7FWS1OlPf0h5UbNgt1Lk9RqHPNUyolgSpz9IuxIEZ98"
|
||||||
|
"1","2026/01/09","クリエイトエスデイ―","-970","三井住友カード (VpassID)","食費","食費","パンとか","0","DUx8pCLFJ5dSXUJU7_cXrKj0h9JXTsr3ZuKT_CugGZ0"
|
||||||
|
"1","2026/01/09","マクドナルド","-340","三井住友カード (VpassID)","食費","カフェ","マクドナルド","0","zw4tfW6NoBw8REKrPxDzG6qZHNur1nQ71mc1ZIvVgPM"
|
||||||
|
"1","2026/01/09","ソフトバンク(B)","-3414","三井住友カード (VpassID)","通信費","インターネット","ソフトバンク光","0","GlgO4AyWXdJ5jf2vpAQw39l-TUuA6st22fqU0dvVPXQ"
|
||||||
|
"1","2026/01/08","MEGAドン・キホーテUNY座間店*","-200","三井住友カード (VpassID)","食費","食費","パンとか","0","MHOm2gdCHCGI1QeTqPu1lQjFQuXESmdgxHSwFhtruMI"
|
||||||
|
"0","2026/01/07","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","9uZXEI3gHuDNuwHvjm5-i6MuBuzhVbZeh_1K2ONdz80"
|
||||||
|
"0","2026/01/07","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","9NwU95XDpPoAKL8Tacy-soUvbokr3bGwPkjO94ASZzs"
|
||||||
|
"1","2026/01/07","定額自動入金","260000","住信SBIネット銀行","収入","自分の口座から移動","月末用資金移動","0","XAFY2ulrVysf8XZQ_M2HslVNB404pLLl9qVuX40reSg"
|
||||||
|
"0","2026/01/06","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","Xe840SsHQvZOPi3JXj35u5qlxk7VdLyKOclslt_bXRM"
|
||||||
|
"0","2026/01/06","オート 座間","3000","モバイルSuica","収入","ポイント","","1","U_fON4aSBlZR2_KLMAHj_vZodLGVPmKdeGgpu93N1uU"
|
||||||
|
"0","2026/01/06","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","BehmIpKy3kDzIPV7ZbY9UwTbehf8ux5wG39syhNN3yM"
|
||||||
|
"0","2026/01/06","小田急電鉄 座間駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","xrC0yyTuMHeEpP2r3TcsM-OEPqyjuMe6F3pxQbu0FYY"
|
||||||
|
"0","2026/01/05","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","vKYYT5_7b5fYWIsFOYgAt8QXHd_d--2c8mJJ6ZprWlg"
|
||||||
|
"0","2026/01/05","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","zeKpJg1wC5RBPTFZOQaOebIMuGOD2TVfPuMYuZ5ALz0"
|
||||||
|
"1","2026/01/05","【指定第2類医薬品】 禁煙補助薬 ニコチネル ミント 90個 販売: Amazon.co.jp","-3944","Amazon.co.jp","日用品","タバコ","ニコチンガム","0","ltlgS4bilKbsI2o5vxgrCyADhDtNR2VGU1xeutc6AXQ"
|
||||||
|
"0","2026/01/05","口座振替 ビューカード","-15000","住信SBIネット銀行","現金・カード","カード引き落とし","","1","jlg6wmecfS042gzK4vv7JFNOEb1bt_YT4UVMSIh3bvc"
|
||||||
|
"1","2026/01/05","約定返済 カードローン","-11000","住信SBIネット銀行","その他","借金返済","借金返済","0","kV0hO-pAqPNZ4M5DDjbHfgQXLQrfxEHit7ZU4eUjmLc"
|
||||||
|
"1","2026/01/04","入 入谷 出 海老名","-147","モバイルSuica","交通費","電車","週末移動","0","jVHOtxHdeDlMXo0dMiCRkxqRskw_63VuusXDuRBHxxA"
|
||||||
|
"1","2026/01/04","入 小海老名 出 本厚木","-136","モバイルSuica","交通費","電車","週末移動","0","ybu_U7HTfFFTkpmqbTj3GRyN_8ErFwxoE_eh1ja_NuQ"
|
||||||
|
"1","2026/01/04","入 本厚木 出 座間","-199","モバイルSuica","交通費","電車","週末移動","0","ljBM963gcXgvUcY8ic-53TcbRBYl2ie-s9v-dEXOii4"
|
||||||
|
"1","2026/01/04","EXC・ホンアツギエキテン","-380","三井住友カード (VpassID)","食費","カフェ","エクセルシオールコーヒー","0","JNclDztkeqGJQR2fIZ5nLj2eFKxfgFkof5YO6tZO4vA"
|
||||||
|
"1","2026/01/04","豚骨らーめんぶたきち 本厚木店","-950","三井住友カード (VpassID)","食費","外食","ラーメン","0","b_7Bmif44jWquMYo-RbhFbxAzDSefrLvdRC0cCQmKpE"
|
||||||
|
"1","2026/01/03","MEGAドン・キホーテUNY座間店*","-1867","三井住友カード (VpassID)","食費","食費","パンとか","0","YQPlAAHlaSdZ1knySIndFbphzaU5z3mWqNqOIKtSBX8"
|
||||||
|
"1","2026/01/03","サイゼリヤ/NFC","-600","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","FsvMHcH6wNiY4-FdjEFJaCRjw-vvZTDqMadi_Tvp3Vc"
|
||||||
|
"1","2026/01/03","2りんかんイエローハット","-1100","三井住友カード (VpassID)","自動車","バイク関連","洗車","0","-metDFUe5pPoQ7Awc_xaxTR6CnWMe3mwUWb6ARp6prA"
|
||||||
|
"1","2026/01/02","ルミネ北千住","-2534","三井住友カード (VpassID)","食費","外食","総菜差し入れ","0","HvpQqPMqmARsGEhz9oZqJj3y_sx71xEy_dHd3WxT0IY"
|
||||||
|
"1","2026/01/02","ルミネ北千住","-1692","三井住友カード (VpassID)","食費","外食","総菜差し入れ","0","Kjz7fphA4lPcFhhutj59fBsqVTuxmZ_Z1dkr6JAT1NA"
|
||||||
|
"1","2026/01/02","ルミネ北千住","-2415","三井住友カード (VpassID)","食費","外食","総菜差し入れ","0","EHooc8WLR_4Gg6J0aN47dxjNo409m_1T6ALZ5hpdwBQ"
|
||||||
|
"0","2026/01/02","オート 地北千住","3000","モバイルSuica","未分類","未分類","","1","R_hLNKLkiagG3IujKJI6LUTEJoAQNC7PRnA9yBmi1iY"
|
||||||
|
"1","2026/01/02","入 座間 出 地北千住","-681","モバイルSuica","交通費","電車","休日移動","0","BWqt5AM6EhXw4YnYJcnRJ4E7gP-xZIcPPSus1icNJ5I"
|
||||||
|
"1","2026/01/02","入 地北千住 出 地 町屋","-178","モバイルSuica","交通費","電車","休日移動","0","YvtD151HoDKrF8VLecZU3FLHLvcI1BFSXZdgE0pe6ZM"
|
||||||
|
"1","2026/01/02","入 地 町屋 出 座間","-681","モバイルSuica","交通費","電車","休日移動","0","j1AsZOAZU-hZ2pBzYFvxs1nB6a-Q8PCynGjaVWyqQA4"
|
||||||
|
"0","2026/01/02","東京地下鉄 北千住駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","CsqFdqHqAI9pBuIA8rwn7FprAWjnztYk284q-yhVOz4"
|
||||||
|
"1","2025/12/31","インターネットイニシアティブ","-1050","三井住友カード (VpassID)","通信費","インターネット","iijmio","0","E_luohEn_5DV-8j0vT_rUD107hD2vx6BIxp59icxvk4"
|
||||||
|
"1","2025/12/31","タイムズカー 202512ゲツガクキホンリヨウ","-880","三井住友カード (VpassID)","自動車","カーシェア","タイムズカー","0","DTpAL7jyXAM7WOd2lV1PrTRli0JR7UJZkxg96VU_WvQ"
|
||||||
|
"1","2025/12/31","キャッシュバック(ポイント交換)","1180","三井住友カード (VpassID)","収入","その他入金","ポイント充当","0","jD0PxWvq77qjKE9U0Z-qGDwPHQrfPkgJCFlC2Ay220w"
|
||||||
|
"1","2025/12/31","サイゼリヤ/NFC","-600","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","26RsT-_TFNWrkWfkDPX0qQ8xd0pJRaNCrh_rK_lA3DQ"
|
||||||
|
"1","2025/12/31","ベロ-チェ 原町田四丁目店/NFC","-330","三井住友カード (VpassID)","食費","カフェ","ベローチェ","0","l5dd8VILHU-qqb6hwG7erwaPnFPA1KLMME5NphZFBRo"
|
||||||
|
"0","2025/12/30","VIEW モバイル","5000","モバイルSuica","未分類","未分類","","1","4wYRyNhSHTFIAH90aR4h2Dst6MYxOPU7eF4AgwZP698"
|
||||||
|
"1","2025/12/30","入 JC静岡 出 JC富士","-590","モバイルSuica","交通費","電車","休日移動","0","Jsafe171jXIOWr2f6FqIVk85rFn3WTi0Sy-i1XhD68o"
|
||||||
|
"1","2025/12/30","入 JC富士 出 JC熱海","-770","モバイルSuica","交通費","電車","休日移動","0","LIxYb-qzTmjeDUnsdoULJssYbMWk-T9ClA_4nsCxk4g"
|
||||||
|
"1","2025/12/30","入 熱海 出 入谷","-1166","モバイルSuica","交通費","電車","休日移動","0","baCi_jh7U3rueixnNhP4w9lfJv9frEjke3XyXUpzKKI"
|
||||||
|
"0","2025/12/30","モバイルSuica入金(チャージ)","-5000","VIEW CARD","現金・カード","電子マネー","","1","IGZaOqS0fMB3aHBMu_kgC-npd6KfqTUPX7WtjfZwU2U"
|
||||||
|
"1","2025/12/30","振込*ユ)スワコウサン","-68000","住信SBIネット銀行","住宅","家賃・地代","家賃","0","miSr91y8l2gJfWb0i0YgWQQYuva0HuM9iQSi0nlAS7Q"
|
||||||
|
"1","2025/12/29","バス等 SJL","-700","モバイルSuica","交通費","バス","休日移動","0","vNT8rJrU4atKGC-c2irz8pK0GQq_cVxUseKIA8Q9HwM"
|
||||||
|
"1","2025/12/29","バス等 SJL","-700","モバイルSuica","交通費","バス","休日移動","0","T3pbQb1XV8VcJKcgtHy9Qk8MdtpZABAp1qf6JlAqMso"
|
||||||
|
"1","2025/12/29","パルシェ・アントレ","-1080","三井住友カード (VpassID)","食費","外食","外食","0","1N4feajPbtZORyjm5HsWItYsaBQ8SHaEmCB3h9_hF6w"
|
||||||
|
"1","2025/12/29","ASTY","-2750","三井住友カード (VpassID)","趣味・娯楽","旅行","おみやげ","0","TjgTYfHUEHu4pShONRntFQIHSC-yHuD__oL0bbwoLTk"
|
||||||
|
"1","2025/12/29","バロー(スーパー)","-2829","三井住友カード (VpassID)","食費","食費","食費","0","WzZ5qwhbiqpf43uTh_wWXZHffOl-EZWllxGhvpCFDhM"
|
||||||
|
"1","2025/12/29","SMBC(スミシンSBIネツ","-260000","楽天銀行","その他","自分の銀行へ移動","月末用資金移動","0","4zaCiyGDuB6FwAm-9Bu1xQS-IahZr99eoT28seUK6vQ"
|
||||||
|
"0","2025/12/29","ラクテンカ-ドサ-ビス","-4269","楽天銀行","現金・カード","カード引き落とし","","1","nvYg3htDtRsAX8MJu4mHKuVmKZF7fUYxTrJTruveJAc"
|
||||||
|
"1","2025/12/28","入 入谷 出 熱海","-1166","モバイルSuica","交通費","電車","休日移動","0","xYz8MG2gg8_00Xu1Gz8cKHx8xIuibAjgQwxyvEs_Bpk"
|
||||||
|
"1","2025/12/28","入 JC熱海 出 JC静岡","-1340","モバイルSuica","交通費","電車","休日移動","0","jIAsnsgkywLuXuhzHOQQHqC-XRidra6oQig2Jkf4V0k"
|
||||||
|
"1","2025/12/28","ドン・キホーテ静岡両替町店","-315","三井住友カード (VpassID)","食費","食費","食費","0","TAmDauhOJL8ekiTqBzrQt6Dc-XObAITcBAeq3nytgUg"
|
||||||
|
"1","2025/12/28","しずてつストア","-1973","三井住友カード (VpassID)","食費","食料品","食費","0","kVJTukVrB-emELOoNpDhNR4KDw9fnpUBEDtrygsnZGY"
|
||||||
|
"0","2025/12/28","ATM セブン銀行","-50000","住信SBIネット銀行","現金・カード","ATM引き出し","現金引き落し","0","YynRpQmlkpPbbs9vXxDWZndSqNeeHzFby5lykeq3rHc"
|
||||||
|
"0","2025/12/28","振込*ナカシマ ユウジ","50000","住信SBIネット銀行","収入","自分の口座から移動","","1","r4F0jjf26nFmyar6K4OcUzTggVz3yLnagXwiVkpdVHU"
|
||||||
|
"0","2025/12/28","住信SBIネット銀行 イチゴ支店 普通預金 3803507 ナカシマ ユウジ(依頼人名:ナカシマ ユウジ 振込予定日:2025年12月28日 管理番号:20251228-96083082)","-50000","楽天銀行","未分類","未分類","","1","Kvktndrmf-0VeslFbBhf2sS-o3l7GbgUK7NkatlqotI"
|
||||||
|
"1","2025/12/28","あみやき弁当","-1500","リアルの個人財布","食費","外食","焼き肉弁当","0","4rVQoRInaWJcjHKHfwCnTKFnTw-bE0-u5Z73W83r19w"
|
||||||
|
"1","2025/12/28","夜のお店","-15000","リアルの個人財布","趣味・娯楽","秘密の趣味","夜のお店","0","Ql3ZVa59ygakHWGxGwp0hdkTrq9pkpOK2UvkybqUW1A"
|
||||||
|
"1","2025/12/27","スギ薬局 相模原南台店","-1080","三井住友カード (VpassID)","健康・医療","薬","心療内科の薬","0","r_3WO4A2GEPPjOeXWThy4FdIPydDDVcu9vBiBvtGqu8"
|
||||||
|
"1","2025/12/27","サイゼリヤ/NFC","-600","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","dh5IwfkOsuffkcS1SeLhvMptjXH-wZRvMZ_y9YcAbH8"
|
||||||
|
"1","2025/12/27","丸亀製麺 座間/NFC","-1310","三井住友カード (VpassID)","食費","外食","うどん","0","j36O4QSCUy45dqu_Pzyf6WPdqMpwCqFNbqQkqNf93zY"
|
||||||
|
"1","2025/12/27","心療内科","-1300","リアルの個人財布","健康・医療","医療費","心療内科","0","eALvPWnOtom6ef5UosVTQce79x3wm3it5Niwlt3HFeg"
|
||||||
|
"0","2025/12/26","オート 小 新宿","3000","モバイルSuica","未分類","未分類","","1","jYeXY1zDnJ0uXLfSkfzMIwUEWSsbpr9p-R3Cg80HMbo"
|
||||||
|
"0","2025/12/26","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","tVx8_YaNDWf-NPvTsRTuxnbRr0hQg4DlsJHi7ZdsjFM"
|
||||||
|
"0","2025/12/26","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","Md45LScLhuoWX7Co-x5uEEAV9lWWuNQ--Cgnf4tfdD4"
|
||||||
|
"1","2025/12/26","振込*ナカシマ ケイコ","-50000","住信SBIネット銀行","その他","恵子の振込み","恵子さんの振り込み","0","rv6fC5hjFVij_2YZnWmzUR-Vw8hb2BYQGW2PPBR4O_0"
|
||||||
|
"1","2025/12/26","普通 円 貯金用口座","-30000","住信SBIネット銀行","その他","天引き貯金","先取貯金","0","bodeUQTzDtEXkeSzwbhIBar6bBJleWhjTjDks-YAfyw"
|
||||||
|
"0","2025/12/26","口座振替 ミツイスミトモカード","-154641","住信SBIネット銀行","現金・カード","カード引き落とし","","1","Hd35llXel6XvHE1LX0ksIGccty_xyvQNv975CBnG5uE"
|
||||||
|
"1","2025/12/26","【指定第2類医薬品】 禁煙補助薬 ニコチネル ミント 90個 販売: Amazon.co.jp","-3944","Amazon.co.jp","日用品","タバコ","ニコチンガム","0","KNiBZfaueKzpdejRmlvbAG9DuudL2j7urfQcqOeMw2g"
|
||||||
|
"0","2025/12/26","小田急電鉄 新宿駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","iQA6iHIhsPjqoQ-WizCT9XbGY-Myt9OdLdPTc0xlnGQ"
|
||||||
|
"0","2025/12/26","ミツイスミトモカ-ド (カ","-104340","三井住友銀行(Olive)","現金・カード","カード引き落とし","","1","3_Aj7RPrZY5o6O7uTHH-tnRDwgArZDhpVpDTOuOtar4"
|
||||||
|
"1","2025/12/25","ピザーラ","-5460","三井住友カード (VpassID)","食費","外食","ピザーラ","0","CnirNFahiZ84yCHF4yZzVudcIQWkPW-4ZZg8vhcqDgw"
|
||||||
|
"1","2025/12/25","株式会社エネライフ","-8060","三井住友カード (VpassID)","水道・光熱費","ガス・灯油代","ガス代","0","ozRfgHnORqZ09Wdu7It2i74zJcp9gbW2LDELYMVx5e4"
|
||||||
|
"1","2025/12/25","給与 カ)ブル-スト-ンリンクアンドサ-クル","341739","楽天銀行","収入","給与","給料","0","Ri9QO11LiAXil4fNbJnzbpbvUD8r8GkTMnWJgzYOLUA"
|
||||||
|
@@ -0,0 +1,187 @@
|
|||||||
|
"計算対象","日付","内容","金額(円)","保有金融機関","大項目","中項目","メモ","振替","ID"
|
||||||
|
"1","2026/02/24","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","","0","XEj2_TNJxLu3nGJNLsV7SJuKHVPON-MpnwfzCGasTwY"
|
||||||
|
"1","2026/02/24","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","","0","TAweLGfV5K38y3ODLMptSfTsJhIMOEMd7muRYeir-HE"
|
||||||
|
"1","2026/02/24","trunk","-1030","三井住友カード (VpassID)","通信費","インターネット","","0","RyiSrCm1vIaGvodR9PQTSBuWjI2bOmzyJsJq6uccsGc"
|
||||||
|
"1","2026/02/24","D J*WSJ-JP (SOUTH BRUNSWI)","-1099","三井住友カード (VpassID)","趣味・娯楽","サブスク","","0","Pw26mD_h35GeWwfGfsiFXe6-wl3bup84j61JHtyTYns"
|
||||||
|
"0","2026/02/24","振込*ナカシマ ユウジ","30000","住信SBIネット銀行","収入","自分の口座から移動","","1","Vmxi7fuxl9tyEqUAHVEP3cVTRWifUQMjUFn6Xas6L7M"
|
||||||
|
"1","2026/02/24","振込*ナカシマ スズコ","-20000","住信SBIネット銀行","その他","仕送り","","0","ubEFelZl5gs9oZSoVSCiV75OzjbPuIqxEPex9aU8hrI"
|
||||||
|
"0","2026/02/24","住信SBIネット銀行 イチゴ支店 普通預金 3803507 ナカシマ ユウジ(依頼人名:ナカシマ ユウジ 振込予定日:2026年02月24日 管理番号:20260224-01138535)","-30000","楽天銀行","未分類","未分類","","1","TQNhvMiIIadP1blPdbZNrY9ZNOS3SjngAPGtz1hWzBo"
|
||||||
|
"1","2026/02/24","利息 ス-パ-フツウ","12","三菱UFJ銀行","収入","その他入金","","0","_W1H-bXp1jvjyp1ghPuZ-o4_KToujnu7vLssAndQUAA"
|
||||||
|
"1","2026/02/23","ハ―ドオフザマテン","-6600","三井住友カード (VpassID)","趣味・娯楽","パソコン","","0","ZAmNSf1psLxdqdaPffAofXtAOLNnCOq2U-0iAnnciJo"
|
||||||
|
"1","2026/02/23","サイゼリヤ/NFC","-580","三井住友カード (VpassID)","食費","外食","","0","mJSsndhuCV_LjNSGlun6K4jtJAc47I8zePWRHdAA6Wk"
|
||||||
|
"1","2026/02/22","ENEOS-SS","-1216","三井住友カード (VpassID)","自動車","ガソリン","","0","b6I7nyfVVeLYnfJUEHWfJrseEGwBCstGxbPBz27LHb8"
|
||||||
|
"1","2026/02/22","ファミリーマート","-185","三井住友カード (VpassID)","食費","飲み物","","0","r9LI3ZL8zUmYnftS3zWnm5grozdu_5HtDt2xsQtklwY"
|
||||||
|
"1","2026/02/22","ピザーラ","-4860","三井住友カード (VpassID)","食費","外食","","0","EeiQFznEFtNddN6f9BvdAWgYUtzctTaJEjmzpCK9VDE"
|
||||||
|
"1","2026/02/22","割引","74","Amazon.co.jp","収入","割引","","0","xtHvK3Q2YUurD1T7lr3j3OssK7lHxXvi8x7NIsZQ5hw"
|
||||||
|
"1","2026/02/22","【第2類医薬品】アレルビ 84錠 販売: Amazon.co.jp","-1482","Amazon.co.jp","未分類","未分類","","0","p-0ur5sHjEnniYfqFh111UzkPN0lWtKSjO2jAl3QKwk"
|
||||||
|
"1","2026/02/22","【指定第2類医薬品】 禁煙補助薬 ニコチネル ミント 90個 販売: Amazon.co.jp","-4070","Amazon.co.jp","日用品","タバコ","","0","hVhRLG1-NntIqtNgGcZlTEJnJoRDm6VSjiZto3hPU0o"
|
||||||
|
"1","2026/02/22","地方税","-3","住信SBIネット銀行","税・社会保障","所得税・住民税","","0","el1t1_VxsOXDLG8Yo82yzJUwWONCccy_zc11SvvzkhM"
|
||||||
|
"1","2026/02/22","国税","-10","住信SBIネット銀行","税・社会保障","所得税・住民税","","0","mlHJVWdPfw55--oH8U68lUlwfYgNuneaaI1IvSv5Qos"
|
||||||
|
"1","2026/02/22","利息","67","住信SBIネット銀行","収入","その他入金","","0","13Uzbbf3LiSqfAWnH-199-kK_7_ksNbTexe6CHajQiU"
|
||||||
|
"0","2026/02/21","AMAZON.CO.JP","-4070","三井住友カード (VpassID)","日用品","タバコ","","1","65emsD8VSO2IERHUxy-xfv6AGa3p3BFU37gXRxuBUWw"
|
||||||
|
"0","2026/02/21","AMAZON.CO.JP","-1408","三井住友カード (VpassID)","日用品","タバコ","","1","rymclccCvXHul4MkvXDqg69vGdDAnQRnxa1hAw_FD54"
|
||||||
|
"1","2026/02/21","MEGAドン・キホーテUNY座間店*","-781","三井住友カード (VpassID)","日用品","日用品","","0","kpzFKn--5mPhesUKNmg1ucLnHJjdLY1b_7ER2ewG1bc"
|
||||||
|
"1","2026/02/21","ハ―ドオフアイコウイシダテン","-1105","三井住友カード (VpassID)","未分類","未分類","","0","nrbofdN_Ff820v7CGi7o2bikgWrXFeIKn9xwxIfitnI"
|
||||||
|
"1","2026/02/21","スギ薬局 相模原南台店","-980","三井住友カード (VpassID)","健康・医療","薬","","0","QsHkssO56qqCQ4pKuLRRAN_tlgmjz5pj-NNdTKywfF0"
|
||||||
|
"1","2026/02/21","サイゼリヤ/NFC","-700","三井住友カード (VpassID)","食費","外食","","0","GtGCM05UrKZQkoLKKWx04A2aeeyXbRE2fzFUqfQpuQg"
|
||||||
|
"1","2026/02/21","普通 円 すずこうざ","15000","住信SBIネット銀行","収入","その他入金","","1","sa55PfS-6Ec0dE09aByxLwfcujFSO-GWPdKJOX4sQdQ"
|
||||||
|
"1","2026/02/20","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","","0","wHRl0fJahT66KYt0WQaeLwgcfGqi_7pSWlxxVmVaGo0"
|
||||||
|
"1","2026/02/20","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","","0","5y3TaNL3rBjj-Cr9C01CjWKJuK0yI8zt8jZAsoOH3ZA"
|
||||||
|
"1","2026/02/20","APPLE COM BILL","-150","三井住友カード (VpassID)","趣味・娯楽","サブスク","","0","Zk1U7VvRBcHTyy8qn0fTOhJh6FbpS72wQbH-v-lkfbY"
|
||||||
|
"1","2026/02/20","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","","0","pN-CdqTGT7kfvuMh4AnBnSkK2QDDSDmhy5ZnwZiFALo"
|
||||||
|
"1","2026/02/20","白髪染め","-800","リアルの個人財布","日用品","日用品","","0","LcemwukWsW_p1YyKe_iCOJDFyWfzOuflm7vXZZXw5nU"
|
||||||
|
"1","2026/02/20","散髪","-1400","リアルの個人財布","衣服・美容","美容院・理髪","","0","G-R1X1PD294nbkTd44igQRI0Dc3BKK5x8U6rx88oqlg"
|
||||||
|
"1","2026/02/19","MEGAドン・キホーテUNY座間店*","-559","三井住友カード (VpassID)","日用品","日用品","","0","BltkwY25Hcdcy8QZYb8o3xMN-Q9xSkDLiJZH9R5NHME"
|
||||||
|
"1","2026/02/19","ヤフーかんたん決済","-8700","三井住友カード (VpassID)","自動車","バイク関連","","0","ZvSu85xkZ8K3qiziG7OdcaL1JbyejHeEeCZMy4tt6AY"
|
||||||
|
"1","2026/02/19","サイゼリヤ/NFC","-800","三井住友カード (VpassID)","食費","外食","","0","ZVdF3qQO-a19MC_ozmLHBkslemtBZWZxOMVgE5VCZ0w"
|
||||||
|
"1","2026/02/18","バス等 ことバス","-1000","モバイルSuica","交通費","バス","","0","mX9E4Fu-c61QWMgOqrglHS8guL2lnMUSINqGTSj1MVs"
|
||||||
|
"1","2026/02/18","入 相模大野 出 座間","-199","モバイルSuica","交通費","電車","","0","wgxLi1QBVPqPzJGi7Cqf4lDN_QrpGZ6FA1Kvxtqr-Jo"
|
||||||
|
"1","2026/02/18","AbemaTV","-1080","三井住友カード (VpassID)","趣味・娯楽","サブスク","","0","mjMwjKHKDleMDVDHrMJW3EVd4YEwZN-rsFf7a064IM0"
|
||||||
|
"1","2026/02/18","京急バス 本社","-1700","三井住友カード (VpassID)","未分類","未分類","","0","AJM47FxxCnLqUeo--CagNT7CA2q4fA_JhHGKD25D398"
|
||||||
|
"1","2026/02/18","ANAFESTA高松ゲート店","-183","三井住友カード (VpassID)","日用品","日用品","","0","6LWDii3-hCGExlHQYgVQFHZYwpBVlRrTqAoePDx4lOM"
|
||||||
|
"0","2026/02/17","AMAZON.CO.JP","-1695","三井住友カード (VpassID)","日用品","タバコ","","1","LDM4Cacse63sHNXUrK847l7U7pZP71o4vmC0tLMXQnY"
|
||||||
|
"1","2026/02/17","バリュードメイン","-2492","三井住友カード (VpassID)","趣味・娯楽","パソコン","","0","hqj9Go68YPO-rVHB0TU_XjKx9JdfqizXhYqu34-pXn0"
|
||||||
|
"1","2026/02/17","フジ","-437","三井住友カード (VpassID)","食費","食費","","0","T6-kpAI0f2eLAcEgIRgtebrK9FVcuTi3RLlZilr3UlY"
|
||||||
|
"1","2026/02/17","蕎麦かっぽう あずみ野","-1690","三井住友カード (VpassID)","未分類","未分類","","0","H5av5woBqS5WN6pwLDVPe6B6NX8sC7UdncKqiDa30HA"
|
||||||
|
"1","2026/02/17","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","","0","VChPyKE0g3v4SdWxxvIbM6mu5wMSmfXkiGgswyFA9N8"
|
||||||
|
"1","2026/02/17","ローソン","-193","三井住友カード (VpassID)","食費","食費","","0","xp2JP9sFcxUtAXUkUuD-96nxxGtSFp7y2McPsiprCBI"
|
||||||
|
"1","2026/02/17","Amazonポイント","1888","Amazon.co.jp","収入","ポイント","","0","N3Z_eCxlsmaN6Y_rcqUrfeGisaiG2QqaAFnLgUuRWeI"
|
||||||
|
"1","2026/02/17","【第1類医薬品】ニコチネル パッチ20 14枚 販売: Amazon.co.jp","-3366","Amazon.co.jp","日用品","タバコ","ニコチンパッチ","0","jvu2pz6NgXh-0LcQs-EnBxJ4fVXh9lz_P9LPpd-Hueo"
|
||||||
|
"1","2026/02/17","グランズレメディグランズレメディ #レギュラー(無香) 50g 並行輸入品 [並行輸入品] 販売: ELSオンライン","-1695","Amazon.co.jp","日用品","日用品","靴のにおい消し","0","oBvXyvTWHsamHJ3izhKVKJr3eiodkwztXfPg0eCjDuo"
|
||||||
|
"1","2026/02/17","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","dxwLtOzyurh35j-R093ZtV5ERYSmgfkSy1w4-gtwITk"
|
||||||
|
"1","2026/02/17","入 新宿 出 品川","-208","モバイルSuica","交通費","電車","出張移動","0","Zwm-NwNCCo0Pnf_bwFoU77slqFS0eKvE1BsRrbhtbmo"
|
||||||
|
"1","2026/02/17","入 京急品川 出 KQ羽1・2","-327","モバイルSuica","交通費","電車","出張移動","0","awf4_b8JnMNw-qqPg3fQh60oHa-lAHEpqzwdWk5t7Xo"
|
||||||
|
"1","2026/02/17","バス等 ことバス","-1000","モバイルSuica","交通費","バス","出張移動","0","5yurH76bNaMA8C5w_FtEFX0t3IYhYzQuUaoYEYknx2c"
|
||||||
|
"0","2026/02/16","AMAZON.CO.JP","-1478","三井住友カード (VpassID)","日用品","タバコ","","1","AaeYZabx9AzR7irLramiEiOYxBvuJO1pFmlaebK64uA"
|
||||||
|
"1","2026/02/16","クリエイトエスデイ―","-5478","三井住友カード (VpassID)","食費","食費","","0","yftQ8lVh-EyW7rfx0tmsRam9rym5daFBClWMAFwTgsg"
|
||||||
|
"1","2026/02/16","普通預金利息","775","三井住友銀行(Olive)","収入","その他入金","利息","0","a8gzJIiLMG9So355p5vABP6FwEY3Gq9VO9wauIgF04I"
|
||||||
|
"1","2026/02/15","ENEOS-SS","-1364","三井住友カード (VpassID)","自動車","ガソリン","","0","tEEF25XfalIgxK1qmiGy22mYyBVZUoanRQsuKhPHxsg"
|
||||||
|
"1","2026/02/15","ソフトバンクM(01月分)","-5645","三井住友カード (VpassID)","通信費","携帯電話","","0","tP22wuTDD2PlPSmSioRCiL7EK4RfSHElPypSDFKo7ac"
|
||||||
|
"1","2026/02/15","ソフトバンクM(01月分)","-993","三井住友カード (VpassID)","通信費","携帯電話","","0","yMnfRNsxWEEft_Hi1BJiWWA62TihShHq1apIiHX5Mi0"
|
||||||
|
"1","2026/02/15","2りんかんイエローハット","-2200","三井住友カード (VpassID)","自動車","バイク関連","","0","Z6mxCdatB7AGB6CgUVgIGxeta8UCJchC7d-hav_5Hmo"
|
||||||
|
"1","2026/02/15","マクドナルド","-590","三井住友カード (VpassID)","食費","カフェ","","0","Fklf9VfD7r9LRIR1bxg7yLW9HhS0dAG7aAvFoyjtAp4"
|
||||||
|
"1","2026/02/15","サイゼリヤ/NFC","-600","三井住友カード (VpassID)","食費","外食","","0","MO5kNaR1Hd6dzMNDwUY5J4hc5l2ZfEy9xkw_ZqA91Kc"
|
||||||
|
"1","2026/02/15","ヤマト運輸株式会社","-1578","三井住友カード (VpassID)","通信費","宅配便・運送","お母さんのPC送付","0","hhJUAcXp3G6Uhobx5QhVbBgrFI72LO3lSkpXBvxfAMw"
|
||||||
|
"1","2026/02/15","ダイソー/NFC","-330","三井住友カード (VpassID)","日用品","日用品","朱肉とか","0","3iVWckVz1hEXNZiffwrnPCJg-mEI_UAXxA2KXp1I-K0"
|
||||||
|
"1","2026/02/14","ヤキタテノカルビ ザマタテノダイテ","-880","三井住友カード (VpassID)","食費","外食","カルビ丼","0","ON_sdPCA07CcYtMcgp8ERqesFeg_NkmXDdXrzyEdpzE"
|
||||||
|
"1","2026/02/14","ハ―ドオフツキミノテン","-1215","三井住友カード (VpassID)","未分類","未分類","","0","LeBJ8-ALq4LMrgNEXufmrEJLbzU4dVwTVOXPc_kDRYU"
|
||||||
|
"1","2026/02/14","ヨドバシカメラ 通信販売(新経路)","-717","三井住友カード (VpassID)","日用品","日用品","","0","3xtjJFffe7kXEQSf7rW5gY1IYdbCNzwVAJhKiwLxa8A"
|
||||||
|
"1","2026/02/14","マクドナルド","-150","三井住友カード (VpassID)","食費","カフェ","","0","KFtb_K_Mv200FXgL1DTtPYZN3-xSI3NcFqbjOay5MyM"
|
||||||
|
"1","2026/02/14","レッドバロン座間","-58270","三井住友カード (VpassID)","自動車","バイク関連","バイクの修理","0","CCDz3coHkcKePfLB5u-Gs3pxwYHiYIqWITmfrr_FlNY"
|
||||||
|
"1","2026/02/14","あいおいニッセイ同和損害保険","-1500","三井住友カード (VpassID)","自動車","自動車保険","バイクの保険","0","4Z6gqxdUGsNtxpYSD29WwP3YH3T4NfQL_H0qery29wM"
|
||||||
|
"1","2026/02/13","現金","6691","モバイルSuica","収入","ポイント","Suicaカードのポイントチャージ","0","AjxT_UThAWjoMKJumwOYdvgfrg1USpIYA7mXNa6KYt8"
|
||||||
|
"1","2026/02/13","UQ mobileご利用料金","-2038","三井住友カード (VpassID)","通信費","携帯電話","UQ mobile","0","YP2PNTpT59yG0Bwqi_9zh_aXzywQWxcmPVH1Z3hD0HI"
|
||||||
|
"1","2026/02/13","SBI証券投信積立サービス","-100000","三井住友カード (VpassID)","その他","投資用振り込み","クレカ積み立て","0","qltGScfqMFCVvpL-ujC3Hs2-1ZJib7W03YOrO1Moxow"
|
||||||
|
"0","2026/02/12","オート 小 新宿","3000","モバイルSuica","未分類","未分類","","1","MYUc_6NbA0PmoNvWwWenxvRQCJcazWdizEvqmo4hJy4"
|
||||||
|
"0","2026/02/12","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","7GgghXWAFUKwFxt0GrTw-aGgdv9fFnOISgxecACgw4M"
|
||||||
|
"0","2026/02/12","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","_6kDrbUm3Bs4aBfj7Bm8eA4v76crzCVPzzZHmO3cPWo"
|
||||||
|
"0","2026/02/12","小田急電鉄 新宿駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","p2EjnCJVSrpRALFUNH7et5NOfCZHp3X_B_Bqp-slRCA"
|
||||||
|
"0","2026/02/11","入 座間 出 小相模原","-167","モバイルSuica","交通費","電車","通勤電車","0","rFp42C1KG0Rweb2gOgsV4H_buZOpzr0qfknAawnc9Fk"
|
||||||
|
"0","2026/02/11","入 小相模原 出 座間","-167","モバイルSuica","交通費","電車","通勤電車","0","fie_OBrIYfjWZ8EuM0_lhQ9blYkV7B-eouLeb-_lSVM"
|
||||||
|
"1","2026/02/11","Amazon Downloads","-819","三井住友カード (VpassID)","趣味・娯楽","本","本の支払い","0","H_TuS6mCyX4FfPoxwjAN5cvTLkYs0315LV8-vb2Qth8"
|
||||||
|
"1","2026/02/11","楽天モバイル通信料","-3193","楽天カード","通信費","携帯電話","楽天モバイル","0","8nlgLKyJO30nrf0_xEk8ZqHeRVqM9YIyr7GtM2WWaVc"
|
||||||
|
"0","2026/02/11","FX戦士くるみちゃん 9 (MFコミックス フラッパーシリーズ) 販売: Amazon Services International LLC","-819","Amazon.co.jp","趣味・娯楽","本","本の購入","0","dhmpUq7PPl3VE3dDEMWfJjpAeuumrFsGVpVjgbGdWUc"
|
||||||
|
"1","2026/02/11","オダサガの喫茶店","-1300","リアルの個人財布","食費","カフェ","喫茶店","0","xc8T86R49IWXg0msZeqyA07gkBtU27b2tGDUKcY3tPc"
|
||||||
|
"1","2026/02/10","MEGAドン・キホーテUNY座間店*","-834","三井住友カード (VpassID)","食費","食費","パンとか","0","u-R0NOSpsxLOR1SHsO2I5XxlAcvPHRBUSC7dPc6d0JA"
|
||||||
|
"1","2026/02/10","でんき(KDDI)","-11345","三井住友カード (VpassID)","水道・光熱費","電気代","電気代","0","QLeD28oa7VGNa6AtqYYXkkZ1K8mmm22ypblgCTv_20I"
|
||||||
|
"1","2026/02/10","UQ mobileご利用料金","-1928","三井住友カード (VpassID)","通信費","携帯電話","UQmobile","0","7VfxU0Kg5q0vVcc9DWCrdV5ObO6fWboQKVsKhMbda8k"
|
||||||
|
"1","2026/02/10","【指定第2類医薬品】 禁煙補助薬 ニコチネル ミント 90個 販売: Amazon.co.jp","-4070","Amazon.co.jp","日用品","タバコ","ニコチンガム","0","SMGhFbUHLia6MIiLQgZl_eBx0_fjoZYlm9gg2yBUznA"
|
||||||
|
"0","2026/02/09","AMAZON.CO.JP","-4070","三井住友カード (VpassID)","日用品","タバコ","","1","RW9V8M9RTAoe4oQC_SG7Pr1Vd93Q54_f87BLBdD3meo"
|
||||||
|
"1","2026/02/09","ANA国内航空券","-94900","三井住友カード (VpassID)","交際費","冠婚葬祭","飛行機代","0","CD-XyQI5RTSmzwVkMOrq1L5umnL51VOOdPTkFvRPdbc"
|
||||||
|
"1","2026/02/09","ANA国内航空券 (返品)","36100","三井住友カード (VpassID)","収入","その他入金","飛行機間違い払い戻し","0","rTGxiVfOzet3RMFt9StbKUmmI0zILlnSEso6s8y_lCc"
|
||||||
|
"1","2026/02/09","ソフトバンク(B)","-3413","三井住友カード (VpassID)","通信費","インターネット","ソフトバンク光","0","fkoOQlYZLSBYvhzkXHfW8RT8NRVAxOTwjPcuzuabkjc"
|
||||||
|
"1","2026/02/09","宮崎空港ビル","-2750","三井住友カード (VpassID)","交際費","冠婚葬祭","リムジンバス","0","zgsiHfTVEpEEbARz9UymzYJCgy58zVDayNAgroL1HTM"
|
||||||
|
"1","2026/02/09","バス等 神奈中","-1700","モバイルSuica","交際費","冠婚葬祭","リムジンバス","0","XamYeDib_9pqqdirfKTX-Jan93xHyGYFrCbbLRlIJCQ"
|
||||||
|
"1","2026/02/09","入 小 町田 出 座間","-199","モバイルSuica","交際費","冠婚葬祭","電車移動","0","XVliwT8Ww0SyW5be91Z_gq_5C_FDvvYJMccYnKXoZT0"
|
||||||
|
"1","2026/02/09","天下一品","-3000","リアルの個人財布","食費","外食","天下一品","0","xm2LA8zNII1wRZH7FyYtXbimdl5_GsiS1TmBjQIh6_g"
|
||||||
|
"1","2026/02/08","宮交タクシー都城3067","-1700","三井住友カード (VpassID)","交際費","冠婚葬祭","タクシー代","0","9uTnuuP3JUnkQIDwcZzJCbPzWz9fMrSNqZOnKJol89Q"
|
||||||
|
"1","2026/02/08","無印良品","-199","三井住友カード (VpassID)","交際費","冠婚葬祭","スプーンとか","0","NJeml_ZzRMTlwYqNmuNK5Pyi5shyM6TKkeWpT0gZ8hc"
|
||||||
|
"1","2026/02/08","イオン九州 SSM","-173","三井住友カード (VpassID)","交際費","冠婚葬祭","ホテルの食べ物","0","dZqT8IxH8b02LI-DHFeh_LUSESYTWP6Jd8Kp-I-5gxw"
|
||||||
|
"1","2026/02/08","イオン九州 SSM","-1802","三井住友カード (VpassID)","交際費","冠婚葬祭","ホテルの食べ物","0","fHFgokGV7_gLBWCQ3ili4ufdWRG_ws0VmRhaF5linGY"
|
||||||
|
"1","2026/02/08","香典","-100000","リアルの個人財布","交際費","冠婚葬祭","香典","0","buuD7_2UcsFmjZrdDbvj-kTjTGJoKTF66S9ojwOqi54"
|
||||||
|
"1","2026/02/07","楽天トラベル 国内宿泊","-10800","楽天カード","交際費","冠婚葬祭","ホテル代","0","ebAYTUUtXNXWLoql3RXBW9CGIZbzqdCZEilZ-NY81FA"
|
||||||
|
"1","2026/02/07","楽天トラベル 国内宿泊","-17100","楽天カード","交際費","冠婚葬祭","ホテル代","0","SwvYfSvo0GbsHRoUIIMAZWVGbbUHSNRnTZrA5xlAwdA"
|
||||||
|
"1","2026/02/07","宮交タクシー都城3085","-1700","三井住友カード (VpassID)","交際費","冠婚葬祭","タクシー代","0","MJNKBkYbMJRSxLHSozop24jUXlyB7vG99QxDGfEx4mg"
|
||||||
|
"1","2026/02/07","MEGAドン・キホーテUNY座間店(専門","-110","三井住友カード (VpassID)","日用品","日用品","香典袋","0","tS3Ad8Mzgz01VuMcOqppO75Zlhr4faUYF8yYf_D76NM"
|
||||||
|
"1","2026/02/07","MEGAドン・キホーテUNY座間店","-5504","三井住友カード (VpassID)","日用品","日用品","靴","0","yJ8gYIXLOrOR7dM8dXrGNNhcPpf8zkYu6xpsNLRpnME"
|
||||||
|
"1","2026/02/07","イオン九州 SSM","-2008","三井住友カード (VpassID)","交際費","冠婚葬祭","ホテルの食事","0","8iOGL17BBa5V0SDwXhTVpWRlNv3uONZdJCZ7S3VFWXc"
|
||||||
|
"1","2026/02/07","宮崎交通/交通利用(バス)/NFC","-1710","三井住友カード (VpassID)","交際費","冠婚葬祭","リムジンバス","0","Q0ORdfCwue52gvrJbzR8Wshah-BCJAJRwsUK1zVCRD4"
|
||||||
|
"1","2026/02/07","入 座間 出 小海老名","-167","モバイルSuica","交際費","冠婚葬祭","電車移動","0","GRAp2GODwqNsnPkTbB3603AouhXgpldwtFvf78gNhq4"
|
||||||
|
"0","2026/02/07","オート 相鉄横浜","3000","モバイルSuica","未分類","未分類","","1","lvs0fhkNUZoQPSJ4nCyCHrK_Yu-uwnYRP98CR3XAMJA"
|
||||||
|
"1","2026/02/07","*入 相鉄海老 出 相鉄横浜","-324","モバイルSuica","交際費","冠婚葬祭","電車移動","0","okUOzYcCWEjt44wJ6_rfE-ipJn5Pm4g-ClzuY4Mr-CU"
|
||||||
|
"1","2026/02/07","入 京急横浜 出 KQ羽1・2","-397","モバイルSuica","交際費","冠婚葬祭","電車移動","0","X_58oa4BbPZqeei2BtVdyy_r72NZxHQCyla7_8wHQdc"
|
||||||
|
"1","2026/02/07","ソラシド エア インターネット","-36100","三井住友カード (VpassID)","交際費","冠婚葬祭","飛行機_日付間違い","0","U04pJeFmg9mWteti2x2fy_7QvTOsQ3wiX7c6vXaPa5I"
|
||||||
|
"1","2026/02/07","ソラシド エア インターネット","-94900","三井住友カード (VpassID)","交際費","冠婚葬祭","葬式の飛行機","0","oo_94apynZAAKPOcUIzkeB1_ANSLR9gSpdjGXbsf5fo"
|
||||||
|
"1","2026/02/07","cuud第2タ-ミナルビル店","-3300","三井住友カード (VpassID)","食費","外食","カレーうどん","0","nASVGcxo-EUc326s5RUDWi4X0U6FwfMt8NSf0xu3qUI"
|
||||||
|
"0","2026/02/07","相模鉄道 横浜駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","icRSEWbkw6jor9FqGTJdgjbvNcnT8J8mlBpuXi36xo4"
|
||||||
|
"0","2026/02/07","ATM ローソン","100000","リアルの個人財布","未分類","未分類","","1","aPz2ZOMVkO90bbG191NkpN6a7RJfZ0Xv4gZd0Fodtw4"
|
||||||
|
"0","2026/02/07","ATM ローソン","-100000","住信SBIネット銀行","食費","食料品","","1","9lhk9BwmE9EFDOjcmWXo64toqTvKgcjsiTdzJpWqDgQ"
|
||||||
|
"1","2026/02/07","普通 円 貯金用口座","120000","住信SBIネット銀行","収入","自分の口座から移動","葬式のための現金","0","iWs2708auiLBsv6zQJYflHnCy_fehTlhVghqggTCnQk"
|
||||||
|
"0","2026/02/06","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","-Dz1_9w7Z_6cEIUkeyDkWLsD3tQ-CigDRNt4C_T4TPc"
|
||||||
|
"0","2026/02/06","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","eBgw7QuMrN8qwQItn-VzeT-K_Ys93hbKQKUd5rDFEws"
|
||||||
|
"1","2026/02/05","約定返済 カードローン","-11000","住信SBIネット銀行","その他","借金返済","借金返済","0","brxsp4vxU8CgPJA8ohch8amLwZ1NtQ4KfRXOdVUKV_8"
|
||||||
|
"0","2026/02/04","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","NIVA0sxQDVdhSEuvaw03JBPHy77pzyvmzLEJ7IIBPaU"
|
||||||
|
"0","2026/02/04","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","b3ba8btXgaM5jchEthVRllFyktW-g43lXbApTg6byP4"
|
||||||
|
"1","2026/02/04","ダイコクドラツグニシシンジユクテ","-494","三井住友カード (VpassID)","食費","お菓子","ガム","0","0n1DFpR7rVrs8flVU0g8FxyQPUDGhkXRfolziijsRRY"
|
||||||
|
"0","2026/02/04","口座振替 ビューカード","-17000","住信SBIネット銀行","現金・カード","カード引き落とし","","1","E-Ol_RgqE0yCSMEfO94Y1yPbOEqOoyrl3Pnj7cdFqXU"
|
||||||
|
"1","2026/02/04","【第1類医薬品】ニコチネル パッチ20 14枚 販売: Amazon.co.jp","-3366","Amazon.co.jp","日用品","タバコ","ニコチンパッチ","0","03qSRvhHAaj9d51JMCGdt5jAyvlDg4Emxt7LcHXx_FQ"
|
||||||
|
"0","2026/02/03","オート 小 新宿","3000","モバイルSuica","未分類","未分類","","1","oz793Lixs_u2hEHchSOKXVeinCWhVVJFlt3qQ6inmk4"
|
||||||
|
"0","2026/02/03","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","zMkszvTTrI-hI6QSyPWofiVQCFAvJo4QB3SYgUW4DHM"
|
||||||
|
"0","2026/02/03","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","leQpc5oo2daHzyEkMMw0620qH32R3JS1fQepB0Gh4BY"
|
||||||
|
"0","2026/02/03","AMAZON.CO.JP","-3366","三井住友カード (VpassID)","日用品","タバコ","","1","lR-LBUHEOadJlLNMcW_lOmUZCQ5fd3pvzNwbbFonpO0"
|
||||||
|
"1","2026/02/03","クリエイトエスデイ―","-181","三井住友カード (VpassID)","食費","食費","パンとか","0","sKRPZwgWGA5pjfIVXPFQDCYWqxE-hIXt1tCG7TVidb0"
|
||||||
|
"0","2026/02/03","小田急電鉄 新宿駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","YLqmCjKYWshJ_rXrw4BD5yosfipT7wVzs9x3xRUnHR8"
|
||||||
|
"1","2026/02/02","定額自動入金","260000","住信SBIネット銀行","収入","自分の口座から移動","月末支払い用","0","nX3vl8d6PGlSuIaIwxfRWq8vNj1F-hv1aYOn2l1xsok"
|
||||||
|
"1","2026/02/02","水道代2か月分","-8000","リアルの個人財布","水道・光熱費","水道代","水道代","0","XvLVgyhuJZCRAVjyvho3AwwlJGCj3jQxljDd-WlICos"
|
||||||
|
"1","2026/02/01","入 小相模原 出 座間","-167","モバイルSuica","交通費","電車","休日移動","0","Mp5FukVIkrLKlgH8ULk_-wT-xgyqDU5x1R_RKnthMiw"
|
||||||
|
"1","2026/02/01","【指定第2類医薬品】 禁煙補助薬 ニコチネル ミント 90個 販売: Amazon.co.jp","-3944","Amazon.co.jp","日用品","タバコ","ニコチンガム","0","cQY90NuVHDhrV-TjHEAXfrJwfo0SCdO1hmmWs1cZ1-w"
|
||||||
|
"1","2026/02/01","サイゼリヤ/NFC","-300","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","zfzGGq6oklKWHLPBi65KIO_FOWyJvY0p2irXEIXo5Nw"
|
||||||
|
"1","2026/02/01","オダサガの喫茶店","-1300","リアルの個人財布","食費","カフェ","オダサガの喫茶店","0","NuHC25ZmkNks8bdm7QUOyKkdti3PUio26I4GJdGBcUc"
|
||||||
|
"1","2026/01/31","インターネットイニシアティブ","-1050","三井住友カード (VpassID)","通信費","インターネット","DIX","0","7VNw9N5hC49Jx12m8tWQ_T3lE-VX4YC-Z-M4biUUKVY"
|
||||||
|
"0","2026/01/31","AMAZON.CO.JP","-3944","三井住友カード (VpassID)","日用品","タバコ","","1","uJsJ5-vywLN2x6uE3z_YTmNx2LKB8qkWn2vhGZ0uEM4"
|
||||||
|
"1","2026/01/31","MEGAドン・キホーテUNY座間店 (返品)","1574","三井住友カード (VpassID)","収入","その他入金","料金間違い","0","rmPuRtlFNCGGWRf98v8Fp3701VZis5UhXb10qbDVj7E"
|
||||||
|
"1","2026/01/31","MEGAドン・キホーテUNY座間店","-1551","三井住友カード (VpassID)","食費","食費","パンとか","0","USPxFadH0nVBn71vePpgYnUX4bSXmVqZEI4uG9W1DoM"
|
||||||
|
"1","2026/01/31","MEGAドン・キホーテUNY座間店*","-1574","三井住友カード (VpassID)","その他","雑費","料金間違い","0","BKY3FnoIYw46WnHnGZeLFgNrg-BCPyyXCY2t5aHbeeg"
|
||||||
|
"1","2026/01/31","ハ―ドオフコウザシブヤテン","-110","三井住友カード (VpassID)","趣味・娯楽","パソコン","インク","0","UgRZM_8HC4qZH8pd3jwS7RdrIhcqgnJ9qWxs0zMs2Fw"
|
||||||
|
"1","2026/01/31","入 さがみ野 出 相鉄大和","-188","モバイルSuica","交通費","電車","休日移動","0","rUPL05gTu-zJGqEtW9fqdE6rPSTfBTMEX8SrIL1TNWY"
|
||||||
|
"1","2026/01/31","入 小 大和 出 高座渋谷","-167","モバイルSuica","交通費","電車","休日移動","0","QCzkMAveeUFth_GOYlvvUdcBEK88KY_5XyiPUlyUAzs"
|
||||||
|
"1","2026/01/31","入 高座渋谷 出 座間","-293","モバイルSuica","交通費","電車","休日移動","0","HhDnVeBQo2y83Z5b5ZrQD_tt1OzD_rrBT47SCVaOcSA"
|
||||||
|
"1","2026/01/30","振込*ユ)スワコウサン","-68000","住信SBIネット銀行","住宅","家賃・地代","家賃","0","-1oiKzjvgmRbnmMAtsJXMQ3JmpuOMbHC5QCZKBoJ9Cw"
|
||||||
|
"0","2026/01/29","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","KaJEXuTTfOQPShRN8CPhTZbLK-ZDLfKPSb_PxhS98J8"
|
||||||
|
"0","2026/01/29","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","AJDZ4VNwyNSilzb46rpMuZVSqacmtXTOrJZTt-kxi50"
|
||||||
|
"1","2026/01/28","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","新宿のドトール","0","gArPPRSRc1aghDl2q40-kvJ3ql0peTOq4m3gTpoY6xc"
|
||||||
|
"0","2026/01/28","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","5VxCrGhjehLXRMxxcWg_2rn0p8rny8Ntpg9Au5cDH6k"
|
||||||
|
"0","2026/01/28","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","eW4HXu5p57RCdhVDoPGhZaJO_QHGjO0kZ58d9wqAg3w"
|
||||||
|
"0","2026/01/27","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","1RhRSnZUMCpfwBA46TbmBLsF5R6Zj0eu4JKR8hzePUU"
|
||||||
|
"0","2026/01/27","オート 座間","3000","モバイルSuica","収入","ポイント","","1","4GlMRHiLXH0Xk4Xt5nRl_P5HTtC8h3V5DvaFBQHlLa4"
|
||||||
|
"0","2026/01/27","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","qAy2afVLWIxwXovrjk4fusD1FV22Nd9wZGHp_D9GIHs"
|
||||||
|
"1","2026/01/27","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","新宿のドトール","0","xTCrK5_4RcfvrcGzG7bWGhB_CPsCWBzcblucnMlYFys"
|
||||||
|
"1","2026/01/27","SMBC(スミシンSBIネツ","-260000","楽天銀行","その他","自分の銀行へ移動","月末資金移動","0","q7ccJVvHEA0AMGpPyV_keUP3sO0dG9r5g6ACtuhe2V8"
|
||||||
|
"0","2026/01/27","ラクテンカ-ドサ-ビス","-15367","楽天銀行","現金・カード","カード引き落とし","","1","ryz5cdURAiBQUrffmPHdJ63cWHkcKV0gOL6JuPA0kd0"
|
||||||
|
"0","2026/01/27","小田急電鉄 座間駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","bn7O3TmxG7bLVBT0UMdChAESpBi7GnxT-vyVn6A0Xx0"
|
||||||
|
"1","2026/01/26","MEGAドン・キホーテUNY座間店*","-784","三井住友カード (VpassID)","食費","食料品","パンとか","0","9oR9XLxnOXvC6E4cF-reYlAKwwA7z0mF3UJW_5xaMWU"
|
||||||
|
"1","2026/01/26","TRADINGVIEWM*PRODUCT (WESTERVILLE )","-21563","三井住友カード (VpassID)","趣味・娯楽","サブスク","TradingView","0","LQBX9cn-0zFP-Mqoa_sfK2gPVC65FWnE_BLA4w9ajUk"
|
||||||
|
"1","2026/01/26","振込*ナカシマ ケイコ","-50000","住信SBIネット銀行","その他","恵子の振込み","恵子さんへの振込み","0","Thr7Ok-Da4MJ1bxflNW90WSEO7qiUyVpChcFKyiEIsk"
|
||||||
|
"1","2026/01/26","普通 円 貯金用口座","-30000","住信SBIネット銀行","その他","天引き貯金","天引き貯金","0","VrdTvrHd5LHyer4HTeMYmo_mQV48a-bk4a2rAqw-lr0"
|
||||||
|
"0","2026/01/26","口座振替 ミツイスミトモカード","-104920","住信SBIネット銀行","現金・カード","カード引き落とし","","1","9cSmRBe5_c8t18RZndg7Xp9P0tTamLkmO_SyhEFsWhg"
|
||||||
|
"0","2026/01/26","ミツイスミトモカ-ド (カ","-101800","三井住友銀行(Olive)","現金・カード","カード引き落とし","","1","VkBH0A1D365ghrGVXkjZYpNLoVipDxliyvssRl9huhc"
|
||||||
|
"1","2026/01/25","物販","-250","モバイルSuica","交通費","駐車場代","駐車場","0","yfTsiQDz3n_YlC82vN6FFhz8AYjAROvaMkpEIhuzs8A"
|
||||||
|
"1","2026/01/25","マクドナルド","-430","三井住友カード (VpassID)","食費","カフェ","マクドナルド","0","8Q45y-kGaoBBivp6turV0qWrF2N_nCUjJxD1FXFbfTM"
|
||||||
|
"1","2026/01/25","MICROSOFT*MICROSOFT","-14900","三井住友カード (VpassID)","通信費","情報サービス","Microsoft365_Classic","0","F4prS7wN3WtAnfy1P_KPhA3HAbEiDJsj9MEqo8RH0ZE"
|
||||||
|
"1","2026/01/25","株式会社エネライフ","-8377","三井住友カード (VpassID)","水道・光熱費","ガス・灯油代","ガス代","0","S-oDviX6611Zfju4a_6ImKV8fq0GvHBXUhEbJLe-hAM"
|
||||||
|
"1","2026/01/25","【第1類医薬品】ニコチネル パッチ20 14枚 販売: Amazon.co.jp","-3430","Amazon.co.jp","日用品","日用品","ニコチンパッチ","0","Bb6EcTxQ7ugU472ea7J7_F2t69hlDwo9nPd7-n0QJcc"
|
||||||
|
"1","2026/01/24","入 座間 出 小相模原","-167","モバイルSuica","交通費","電車","休日移動","0","vee1a8IHfIHx1AbBo-FnwkkmvMjI_nJb7YxAZplRyCc"
|
||||||
|
"1","2026/01/24","入 小相模原 出 小 町田","-167","モバイルSuica","交通費","電車","休日移動","0","uo5jeNbPCHYM8MKNZ3pNQVXoHUzGAgMR0HbGrHfkrbo"
|
||||||
|
"1","2026/01/24","入 小 町田 出 座間","-199","モバイルSuica","交通費","電車","休日移動","0","K4wrgvfMG9tQYzMY5Vf7wRBdneJN6DZXnhipqC_RXag"
|
||||||
|
"1","2026/01/24","魚べいさがみ野店","-5874","三井住友カード (VpassID)","食費","外食","寿司","0","ijtIItCrLDHr0pQnRAYYN7wE1CBTcyZmnbBkVvmVKcI"
|
||||||
|
"0","2026/01/24","AMAZON.CO.JP","-3430","三井住友カード (VpassID)","日用品","タバコ","ニコチンパッチ_支払い","1","phqLBAk5bEprg1X6KUUYkWv6czVX6cNTG-xw54PS1gk"
|
||||||
|
"1","2026/01/24","MEGAドン・キホーテUNY座間店*","-959","三井住友カード (VpassID)","食費","食料品","パンとか","0","IAUOInXclCYNas9EzqWrrRgdGZbZnN9hP35q4gGaW1Q"
|
||||||
|
"1","2026/01/24","タイムズカー 202601リヨウリヨウキン","-1760","三井住友カード (VpassID)","自動車","カーシェア","カーシェア","0","M96yYR0RXlCCt6thLCkTp6ehfdaxkXGPpeeUEsHNBlE"
|
||||||
|
"1","2026/01/24","trunk","-1030","三井住友カード (VpassID)","通信費","インターネット","DIX","0","C5OHmwfQzcWLp6Kqsv5jULA3Z8H18iUi8UXyREgxGA8"
|
||||||
|
"1","2026/01/24","サイゼリヤ/NFC","-600","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","9ZM_2DWfdWxLtuStCsexqxw7lGuzD2cPxdHiGeWj0P8"
|
||||||
|
"1","2026/01/24","D J*WSJ-JP (SOUTH BRUNSWI)","-1099","三井住友カード (VpassID)","趣味・娯楽","サブスク","WSJ","0","NJSDEcvxW8j_Lt0U1tlvgp8pYc4vzj5Fx3cFzHWJ2AY"
|
||||||
|
"1","2026/01/24","【指定第2類医薬品】 禁煙補助薬 ニコチネル ミント 90個 販売: Amazon.co.jp","-3944","Amazon.co.jp","日用品","タバコ","ニコチンパッチ","0","zHQVHsfURwy6oCN9hgFuJfJCeGq4KDOWxoOrDklcjFw"
|
||||||
|
"1","2026/01/24","町田のカフェ","-380","リアルの個人財布","食費","カフェ","町田のカフェ","0","IaIxCRYm5fIXGgANr9a3gy0I8xEDeZOAhN-FsvsOOKc"
|
||||||
|
"1","2026/01/24","心療内科の薬","-1100","リアルの個人財布","健康・医療","薬","心療内科の薬","0","c1NLlBaJzMxjbi6D5KiHg-mAl0isnQED2ccYtYnxYFc"
|
||||||
|
"0","2026/01/23","AMAZON.CO.JP","-3944","三井住友カード (VpassID)","日用品","タバコ","ニコチンパッチ_支払い","1","IbvhCktAyKQZWF845nbwXpu68KfnyE6prtxj3Mm8HFY"
|
||||||
|
"1","2026/01/23","給与 カ)ブル-スト-ンリンクアンドサ-クル","309848","楽天銀行","収入","給与","給料","0","mEhN0jhHL5rE7m9DWqAQsLAxsnP0o8exQtcb8ytepwk"
|
||||||
|
@@ -0,0 +1,123 @@
|
|||||||
|
"計算対象","日付","内容","金額(円)","保有金融機関","大項目","中項目","メモ","振替","ID"
|
||||||
|
"0","2026/03/24","小田急電鉄 座間駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","xOOR5OVSKo2fDyj0Al1b2bTRsdR67-jLGazIRFCvwOM"
|
||||||
|
"1","2026/03/24","D J*WSJ―JP","-1099","三井住友カード (VpassID)","趣味・娯楽","サブスク","","0","jFR-KRt_qOOXH5ax01KnsGUSi23fV4KgG2CavS8vGCk"
|
||||||
|
"0","2026/03/23","振込*ナカシマ ユウジ","250000","住信SBIネット銀行","収入","自分の口座から移動","","1","6TRgLsBZECKqlvfmR3O8NmTiSCekM4dMJHwEMHC26T0"
|
||||||
|
"1","2026/03/23","普通 円 貯金用口座","50000","住信SBIネット銀行","収入","自分の口座から移動","","0","wdIF3TLwWEwbaTLXH-_eHqlGJh3scpyr81mVmr3QrOI"
|
||||||
|
"0","2026/03/23","住信SBIネット銀行 イチゴ支店 普通預金 3803507 ナカシマ ユウジ(依頼人名:ナカシマ ユウジ 振込予定日:2026年03月23日 管理番号:20260323-04017908)","-250000","楽天銀行","未分類","未分類","","1","kyiEpelo-tTiByYzD56TaYVq5AwbMtKA4ZabV-N4iP0"
|
||||||
|
"1","2026/03/22","MEGAドン・キホーテUNY座間店*","-591","三井住友カード (VpassID)","食費","食料品","","0","XwDFJ7Q18l5oGBrIEz7Cehvtw9ay5H2RNUn-2l-dpcM"
|
||||||
|
"1","2026/03/22","ENEOS-SS","-1723","三井住友カード (VpassID)","自動車","ガソリン","","0","4UQWsOi2ui4hGGYBRI3ihw8p0DTxMh23zXEH3Pzl8lY"
|
||||||
|
"1","2026/03/22","ANTHROPIC","-809","三井住友カード (VpassID)","未分類","未分類","","0","SEclqZJ1ywbxT04D35HHLtkU80SVdQ-FOLZiJVB5gH4"
|
||||||
|
"1","2026/03/22","ベックス橋本店","-350","三井住友カード (VpassID)","未分類","未分類","","0","H8z-AIU_wOU4600E4vslyViSuOB9ivi6UbDd0gskPrU"
|
||||||
|
"1","2026/03/22","サイゼリヤ/NFC","-600","三井住友カード (VpassID)","食費","外食","","0","9neP_ChTsqbcgoxC1qgpOpJyLNSJi8Kc4pHjLQe80aw"
|
||||||
|
"1","2026/03/22","地方税","-2","住信SBIネット銀行","税・社会保障","所得税・住民税","","0","eJWoMcRyWtN3frwv5VV3st8eNtzJGiq9Zqu7Korbr3I"
|
||||||
|
"1","2026/03/22","国税","-8","住信SBIネット銀行","税・社会保障","所得税・住民税","","0","X5mnJ_tP3w7zz0D4KewL2tlw6l_zfCwFRV13CMuFQWI"
|
||||||
|
"1","2026/03/22","利息","54","住信SBIネット銀行","収入","その他入金","","0","3iKYTwQ_HW8ZILE9BBwlmyRVbv3V2S6gEk4_HRQbBak"
|
||||||
|
"1","2026/03/22","口座振替(楽天カ-ド以外の引き落とし:1-2件)ボ-ナス金利利息","5","楽天銀行","収入","その他入金","","0","RNMsMDGYcT2nSrc4mL2Cc823PKUxsHqwdAv6qy2Rfio"
|
||||||
|
"1","2026/03/22","給与・賞与・年金受取ボ-ナス金利利息","15","楽天銀行","収入","その他入金","","0","YxkIjdAvIn-W249KFpylQWYhXorX2pFhCWsBRZVKQrE"
|
||||||
|
"1","2026/03/21","AMAZON.CO.JP","-3873","三井住友カード (VpassID)","日用品","タバコ","","0","tWK7GCjxJlyx4CGChqM3EMKQ-LJJYoTDYtSOsac-POE"
|
||||||
|
"1","2026/03/21","MEGAドン・キホーテUNY座間店*","-749","三井住友カード (VpassID)","食費","食料品","","0","VMozRt49IoDLHxZarCUtD_CgqK426mSzig4Lwv_Mgys"
|
||||||
|
"1","2026/03/21","サイゼリヤ/NFC","-580","三井住友カード (VpassID)","食費","外食","","0","0_hyFrOsxtgPk9evJvkaVbfSmVSvIBnthC1Wbrfs8bs"
|
||||||
|
"1","2026/03/20","APPLE COM BILL","-150","三井住友カード (VpassID)","趣味・娯楽","サブスク","","0","R0fh46qZdGZMlCxH6OE8Jry3x9y2sFQjTJbcgoKfMek"
|
||||||
|
"1","2026/03/18","AbemaTV","-1080","三井住友カード (VpassID)","趣味・娯楽","サブスク","","0","0z3QXaatJiwgt3VZn8hmGn7UmjJTvHEF11SMHvDZIEE"
|
||||||
|
"1","2026/03/18","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","","0","I14FXX8kQmIM6eZFz-i2b8Tt15GW5lHYQwzSBjLBCKE"
|
||||||
|
"1","2026/03/17","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","","0","s8ck70vHI6qbshvACxcZaIQHlakcjiQNxdJkd64v5vo"
|
||||||
|
"1","2026/03/17","小田急電鉄 新宿駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","0","NbVokdUksa-2sf6mex2jiscoyuulq45JwOvj2sV1LzE"
|
||||||
|
"1","2026/03/16","ROY UNION LIMITED","-4380","三井住友カード (VpassID)","未分類","未分類","","0","4FUZUtyPTXtP3MqwQp4wQpLc3XbKVbaw_XfyuB2mk0o"
|
||||||
|
"1","2026/03/16","MEGAドン・キホーテUNY座間店*","-559","三井住友カード (VpassID)","食費","食料品","","0","dL8l1_cmqjC6qSNGOVVQbp1scEfA78mfp3yixJGW66Y"
|
||||||
|
"1","2026/03/16","Vサガク211067","100","三井住友銀行(Olive)","未分類","未分類","","0","tSA_OADi_OujJV5CDqGIrVOZ19JGjqcXTLS_a0OmJH4"
|
||||||
|
"1","2026/03/15","MEGAドン・キホーテUNY座間店*","-503","三井住友カード (VpassID)","食費","食料品","","0","cCWXFqKKKxXVgoRCJUJ6pbVxuUDv_mwXm4FmyfXEDeU"
|
||||||
|
"1","2026/03/15","ソフトバンクM(02月分)","-993","三井住友カード (VpassID)","通信費","携帯電話","","0","ILqPC-IeKPv4wIR4EdQjO0XYeC_DWoTtzq9L3cBloiI"
|
||||||
|
"1","2026/03/15","ソフトバンクM(02月分)","-5645","三井住友カード (VpassID)","通信費","携帯電話","","0","jUP1HlEsTXMrprY1GKBJLCe0VSCmuU3COoRdIDopQ2Q"
|
||||||
|
"1","2026/03/15","セリア マルイファミリ-海老名店","-110","三井住友カード (VpassID)","日用品","日用品","","0","b6cGB61PpF-YhW4avLzuVuhVW9hmlBFepgC7aqZ2lNQ"
|
||||||
|
"1","2026/03/15","座間2りんかん","-550","三井住友カード (VpassID)","自動車","バイク関連","","0","QSjV4mMk8NJ3Hokvh5yqEsyUsNirSFD15fOT90LKNKg"
|
||||||
|
"1","2026/03/14","AMAZON.CO.JP","-4070","三井住友カード (VpassID)","日用品","タバコ","","0","xFjkgXhgseSLRp39EtuN1uGROP8vIeQOLahvpP2MzoU"
|
||||||
|
"1","2026/03/14","MEGAドン・キホーテUNY座間店*","-1096","三井住友カード (VpassID)","食費","食料品","","0","OCEByEGFWP9N8yfFeHIRpadl95JBJDZCiXz532hBh54"
|
||||||
|
"1","2026/03/14","スギ薬局 相模原南台店","-1080","三井住友カード (VpassID)","健康・医療","薬","","0","Wwadvwch7gvpPIrFxtoRkB6fKAkF_aU4TAvmD0jVODU"
|
||||||
|
"1","2026/03/14","1217マツモトキヨシ小田急相模原駅前店","-2078","三井住友カード (VpassID)","日用品","ドラッグストア","","0","MlrzOHnAVE3qbq5NU9uKTDFUBhyCOvkBbZsbITz6hY4"
|
||||||
|
"1","2026/03/14","あいおいニッセイ同和損害保険","-1500","三井住友カード (VpassID)","自動車","自動車保険","","0","e7eNM_QDl5q2aOoGXly6faIQtg_csb4aqDmXJXQyl-Y"
|
||||||
|
"1","2026/03/14","サイゼリヤ/NFC","-800","三井住友カード (VpassID)","食費","外食","","0","I06c0PE4oYstoaghziuLqBTEf1lBS9sArBfnx4iK4KA"
|
||||||
|
"1","2026/03/14","入 座間 出 小相模原","-167","モバイルSuica","交通費","電車","休日移動","0","tM9FJCOoNFYJPAYGCHP0tkq73FGRNx3_iT_UgBRKHng"
|
||||||
|
"1","2026/03/14","入 小相模原 出 座間","-167","モバイルSuica","交通費","電車","休日移動","0","sYNb-r-dqcdpUM2P-iXOkt0Smv_OIC66SpQv7GsQGwM"
|
||||||
|
"1","2026/03/13","MEGAドン・キホーテUNY座間店(専門","-110","三井住友カード (VpassID)","日用品","日用品","","0","zlTUWCiY9UvLQjW-bGsvtffIaNNCOWV25bgC7xRrFFI"
|
||||||
|
"1","2026/03/13","MEGAドン・キホーテUNY座間店","-474","三井住友カード (VpassID)","食費","食料品","","0","Y2sweqGbwz7dbpdiZ0uqAzMgRIxNaNmwCgSWT2rb-mU"
|
||||||
|
"1","2026/03/13","MEGAドン・キホーテUNY座間店*","-574","三井住友カード (VpassID)","食費","食料品","","0","LKWQXQQWn8n0OQaK-F57Du8O5hCVimORYCEKidnZFx0"
|
||||||
|
"1","2026/03/13","SBI証券投信積立サービス","-100000","三井住友カード (VpassID)","その他","投資用振り込み","積み立て投資","0","D-8Rj2KWGCmNMYiLGNN-Leg4NkzWGrsDEkn5EMYx-T8"
|
||||||
|
"1","2026/03/13","UQ mobileご利用料金","-2272","三井住友カード (VpassID)","通信費","携帯電話","UQmobile","0","Fb95YUmP8yf2INrvahPBebz6-MBk4h_Duv3vgTLuh44"
|
||||||
|
"1","2026/03/12","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","ドトール","0","jCbQNH6-WhNu4NdlRsNIWFNWPeHmu8YiE3jX8k9zC7w"
|
||||||
|
"0","2026/03/12","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","JL23hBJxFEQ6hMD8CAwGiPrY4YCv6fRTGjfowJUvPfs"
|
||||||
|
"0","2026/03/12","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","TZ4pe6LWj98GAhWx3yFELU8jOsPn0BBRHtKYblnbFFc"
|
||||||
|
"1","2026/03/12","AMAZON.CO.JP","-1478","三井住友カード (VpassID)","日用品","タバコ","","0","P-9ul0cd4Zll_R85EG2xlo-XM_ZX1ndEW9mRJZQeYmk"
|
||||||
|
"1","2026/03/11","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","ドトール","0","AaZVMnc0v-cRqIFbYvhamWHFBmZMWdyo0Q8hM9rt5vY"
|
||||||
|
"0","2026/03/11","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","nHzxwzqkYBs_FUIRjvDwslWML1sSlm_mvUk1PWPVsr0"
|
||||||
|
"0","2026/03/11","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","bewSDiBvma1H4wMpZAVrsKcU9lZl_0cmsdx4VUl1fAw"
|
||||||
|
"1","2026/03/11","楽天モバイル通信料","-1885","楽天カード","通信費","携帯電話","楽天モバイル","0","mAuur-Kj8TiN171C49CsQmSx9jiNOpO07aQtTQCTRdU"
|
||||||
|
"0","2026/03/10","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","ggN5-etJpl0U_UyqjlLJHiJoW__kdbQ_s1eoC3lW6C0"
|
||||||
|
"0","2026/03/10","オート 座間","3000","モバイルSuica","収入","ポイント","","1","oZadWjv19kOJdFqMf18IqQZLfetYbWKaoAEHqVPmlAk"
|
||||||
|
"0","2026/03/10","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","HOnC7aDHRFNIvIrU97gSZ-6ki0Ndm88cC0DwGy76r3I"
|
||||||
|
"1","2026/03/10","AMAZON.CO.JP","-337","三井住友カード (VpassID)","日用品","タバコ","","0","tgPOmRJmWr5LeDpIRnPFjQ5uQDmPxzEbPswBBtJhQEI"
|
||||||
|
"1","2026/03/10","ダイコクドラツグニシシンジユクイ","-409","三井住友カード (VpassID)","食費","お菓子","ガム","0","716mnoBKTVhdvY9RhO5TmnxLvU3BK6e0v8S4R8W04VE"
|
||||||
|
"1","2026/03/10","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","ドトール","0","eEUGloIib0_JHEmgPvJ8xfrirETbKWeg0smDmkoM6OA"
|
||||||
|
"1","2026/03/10","でんき(KDDI)","-19784","三井住友カード (VpassID)","水道・光熱費","電気代","電気代","0","tm2FElbYBVTbQwKy8-OojtwrTCto4vl4ZdkZIXbaSEo"
|
||||||
|
"1","2026/03/10","UQ mobileご利用料金","-2313","三井住友カード (VpassID)","通信費","携帯電話","UQmobile","0","hwB52C5bk7upIRJb01iLreoxBWX1evb57JfdLipq6KY"
|
||||||
|
"0","2026/03/10","小田急電鉄 座間駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","hlf-_s_Ey8FHd_eMR1VYD6y93apm1ptiCBUrL3SCa0M"
|
||||||
|
"1","2026/03/10","振込2 ヤフ-ケツサイ","14838","三菱UFJ銀行","収入","物品売却","ヤフオクの売り上げ","0","rFV45WBhvDyo2Y8dVMTDr2HhqNE_3KOURUcBrPJEXqM"
|
||||||
|
"1","2026/03/09","AMAZON.CO.JP","-2753","三井住友カード (VpassID)","日用品","タバコ","","0","BA2Zr752V4__diSS7F3XqW8bUyvpwkbyCZS6mWxeE1s"
|
||||||
|
"1","2026/03/09","ソフトバンク(B)","-3413","三井住友カード (VpassID)","通信費","インターネット","ソフトバンク光","0","UXFkIOsqlX__YOYx9RbUTBok0StitBPOiCghsguduHU"
|
||||||
|
"1","2026/03/08","MEGAドン・キホーテUNY座間店*","-341","三井住友カード (VpassID)","食費","食料品","パンとか","0","LiWsmbSdN_1bgaru8FWGuQXUbk3hoxB-1niClkRe4G8"
|
||||||
|
"1","2026/03/08","サイゼリヤ/NFC","-1100","三井住友カード (VpassID)","食費","外食","サイゼリヤ","0","ycBqix7XiqdgPqNipTJh2QRG5MQgGopgmln7r-qe7Ds"
|
||||||
|
"1","2026/03/08","PE 大和税務署","-351800","楽天銀行","税・社会保障","その他税・社会保障","贈与税","0","F-LiiVmyoBe2Mq8-zIx40Nh3rMABVIXqiVeYQh6-f7M"
|
||||||
|
"1","2026/03/07","イオンモール座間","-330","三井住友カード (VpassID)","日用品","日用品","のど飴","0","4PF4u-jQ9AYdPf3-607cEyTx5xQuyMcnEK_-FF4m0J0"
|
||||||
|
"1","2026/03/07","MEGAドン・キホーテUNY座間店*","-1648","三井住友カード (VpassID)","食費","食料品","パンとか","0","-OBNZR1taqe1MNOW-DdMWSLj2HewGbLgt7ClBc2pzgo"
|
||||||
|
"1","2026/03/07","イオンリテール","-360","三井住友カード (VpassID)","食費","カフェ","カフェ","0","320_V4XVu7EKX37zuQcJsGxq54xpUPLr-7ZC99tgM84"
|
||||||
|
"0","2026/03/06","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","eCgvzMlXL9R-39DlUvSn7aFoEGw6GlIEhkruPAOMYgc"
|
||||||
|
"0","2026/03/06","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","hNm5N87cUY6XfZmsSFf-1jFsdC6pAzI7shOhlijLczE"
|
||||||
|
"1","2026/03/06","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","ドトール","0","L0voTXif7EIL7GSnjJW7K3YbvwFBwsyHyZUK9bMifds"
|
||||||
|
"1","2026/03/05","マクドナルド","-340","三井住友カード (VpassID)","食費","カフェ","マクドナルド","0","AQQXKhiXbePpMDP1OJQN5vouMOoeLh9bh5qh_QP2MAw"
|
||||||
|
"1","2026/03/05","MEGAドン・キホーテUNY座間店*","-1636","三井住友カード (VpassID)","食費","食料品","パンとか","0","wwx3bg5nHg5w_jxLv-1wgh3lArlZq7oaItPt_SHJMh4"
|
||||||
|
"1","2026/03/05","定額自動入金","260000","住信SBIネット銀行","収入","自分の口座から移動","月末資金移動","0","PrKZWUNCUbfQHH4VvC17oNx31XxOSd00-NR7Gg5MJj0"
|
||||||
|
"1","2026/03/05","約定返済 カードローン","-11000","住信SBIネット銀行","その他","借金返済","借金返済","0","DudEdXgeDVuCR7hI2tAAoCSJM-YF3wh76rpYDwlu7lc"
|
||||||
|
"0","2026/03/04","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","odkqNnzT-doXOUMlEJ5rpNxmn4QPGiQiITamSoib3is"
|
||||||
|
"0","2026/03/04","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","tUrmFDV_f8Dip1GKE4Tx88rDTvgiS3tthvsp80Sp5Ck"
|
||||||
|
"1","2026/03/04","AMAZON.CO.JP","-3809","三井住友カード (VpassID)","日用品","タバコ","","0","z8Y79oNruWpEVgxPcZfbcXdHgLpYPp_efZ63tcRsUIE"
|
||||||
|
"0","2026/03/04","口座振替 ビューカード","-15000","住信SBIネット銀行","現金・カード","カード引き落とし","","1","BLd3cDETvpesXppzzrP9TwYyE14mS1MFaUl017yZWFg"
|
||||||
|
"1","2026/03/04","振込2 ヤフ-ケツサイ","2750","三菱UFJ銀行","収入","物品売却","オーディオミキサー売った","0","xsf_REneIwYhudNu_1bvwhbz1YKqUnac2F2yMLt4oaQ"
|
||||||
|
"0","2026/03/03","オート 小 新宿","3000","モバイルSuica","未分類","未分類","","1","UMRUar6lJato_VMYVtYhTrAI1QWqNp5PZAHBZcvLKLc"
|
||||||
|
"0","2026/03/03","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","QtSplWaZkg7BtGljovFRXsYwsY9T40YfoTawKsuVYxU"
|
||||||
|
"0","2026/03/03","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","AzrVmghzCfeIiTPAG9BKfhCHd1NTnTF70Ju1IY9jCrc"
|
||||||
|
"1","2026/03/03","Amazonプライム会費","-5900","三井住友カード (VpassID)","趣味・娯楽","サブスク","アマゾンプライム","0","CmgmZqBUzOATjkAptP5g0Yvuk_7ZtlaVdd2OVFa4gkc"
|
||||||
|
"1","2026/03/03","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","ドトール","0","pejbVnBWKvf-E0kLUsh4YBItUDmIogUGs_5JZPKhqhs"
|
||||||
|
"0","2026/03/03","小田急電鉄 新宿駅 オートチャージ(モバイル)","-3000","VIEW CARD","現金・カード","電子マネー","","1","8TtpmVlN_BKa5p3pJmfTgZCpVsemLnV2of-iIt6sP2c"
|
||||||
|
"1","2026/03/03","京王モ-ル/NFC","-537","三井住友カード (VpassID)","日用品","日用品","ガム","0","-PTYUkQ671IWyTuiEVnl_HhDH_swfT5uBKe0etXFMgc"
|
||||||
|
"1","2026/03/03","振込2 ヤフ-ケツサイ","5000","三菱UFJ銀行","収入","物品売却","ドラレコ売った","0","Fr_-vriPODRtB85RU5VaisTZqHC7KUKL0DnJMeDRgRo"
|
||||||
|
"0","2026/03/02","AMAZON.CO.JP","-2580","三井住友カード (VpassID)","日用品","タバコ","","1","HICKjq6x5SIUxT_VKNOe8GVrHyEDjua4j1PM2iO9NFk"
|
||||||
|
"1","2026/03/02","クリエイトエスデイ―","-2151","三井住友カード (VpassID)","健康・医療","ボディケア","クレアチン","0","0zkV5aAwQ5hHVgI0ItAAu0y6niS9MuyawxuT4YV972g"
|
||||||
|
"1","2026/03/02","CLAUDE.AI SUBSCRIPTI","-3188","三井住友カード (VpassID)","趣味・娯楽","サブスク","Claude","0","PkC3v1DMTC3fEFqFbLupdrfRN3zvXX_d5jOEXncI2X4"
|
||||||
|
"1","2026/03/02","配送料・手数料","-200","Amazon.co.jp","その他","雑費","Amazon急ぎ配達","0","DW4AMeYbBvfkFu3VDGTElqMwZy6RThpQODaC3SzcN88"
|
||||||
|
"1","2026/03/02","クレアチン モノハイドレート 1000000mg Wout ワウト 1000g 200食分 GMP認証 ウルトラ ピュア パウダー 99.9% 無添加 販売: FeelLab (フィールラボ) 適格請求書登録番号 T3180003018432","-2380","Amazon.co.jp","健康・医療","ボディケア","クレアチン","0","20xcj0HfWX7Ky2IQufOyn65p4fBz5tfkK9sAgDjK7T4"
|
||||||
|
"1","2026/03/01","MEGAドン・キホーテUNY座間店*","-665","三井住友カード (VpassID)","食費","お菓子","ガム","0","Ve4nO7G63Rj8EJVWIURR6JHfFmf4RQz_oLlKEq8q9SU"
|
||||||
|
"1","2026/02/28","ETC首都高","-710","三井住友カード (VpassID)","自動車","道路料金","高速道路","0","iaIPZe1fWvzeDAIHyKKrx7CXszDXi-M5NDyw7B409tQ"
|
||||||
|
"1","2026/02/28","インターネットイニシアティブ","-1050","三井住友カード (VpassID)","通信費","インターネット","iijmio","0","LlLk00gRedgMQqiHhbvfSZVRRu92dMfKk607lLXIXnM"
|
||||||
|
"1","2026/02/28","タイムズカー","-880","三井住友カード (VpassID)","自動車","カーシェア","カーシェア","0","5X3rCcMHOJTzUT3HxipnY8KihOGNRzjrUKA39XbmUXQ"
|
||||||
|
"1","2026/02/28","キャッシュバック(ポイント交換)","1502","三井住友カード (VpassID)","収入","その他入金","ポイント充当","0","EFjCrMA9oDhckokuB3AwEds-PgmBINy2ltVaFjrxpy8"
|
||||||
|
"1","2026/02/28","ゴーゴーカレー川崎モアーズスタジアム","-1180","三井住友カード (VpassID)","食費","外食","ゴーゴーカレー","0","vSWCYbBCWSGUCXq_FKnPwmbEL6q9bKo_LMeIfeYV0p8"
|
||||||
|
"1","2026/02/28","サンワ ラゾ―ナカワサキテン","-276","三井住友カード (VpassID)","食費","お菓子","羊羹","0","wsfE5_AyyrTnSsMF6HYRrorj_1ZfbOD2TzsOsiWE9eY"
|
||||||
|
"0","2026/02/27","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","6Q8BUf3vwzv-DT4EUu2-ke0eQJWJLeL4cNOO8RvflAs"
|
||||||
|
"0","2026/02/27","入 新宿 出 大宮","-483","モバイルSuica","交通費","通勤電車","通勤電車","0","RxrGXkEx7Cen1DpGFU9SppImhiCs0VGcl9hMM7KVEAA"
|
||||||
|
"0","2026/02/27","入 東武大宮 出 豊春","-261","モバイルSuica","交通費","通勤電車","通勤電車","0","nWTXqa72YCy4ZCAWa9RpvE0PyPBbq8oGGCc2NnwiKqk"
|
||||||
|
"0","2026/02/27","入 豊春 出 東武大宮","-261","モバイルSuica","交通費","通勤電車","通勤電車","0","ZXmLVe8FzBvXmnbCjln760aiSNnlou1QTWYKXxcK7K4"
|
||||||
|
"0","2026/02/27","入 大宮 出 新宿","-483","モバイルSuica","交通費","通勤電車","通勤電車","0","OISnXoYQ04gHGmk3T42O-jzlMP4mIG76ork58A00sgk"
|
||||||
|
"0","2026/02/27","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","_4d5Do9a07S8kCubbaRSuFR3FcZph2WFjNp3NVAS9G0"
|
||||||
|
"1","2026/02/27","振込*ユ)スワコウサン","-68000","住信SBIネット銀行","住宅","家賃・地代","家賃","0","jXlGz4537jPHIu3TWFC0haYh-XMddNhkFVvc5kPXswo"
|
||||||
|
"1","2026/02/27","SMBC(スミシンSBIネツ","-260000","楽天銀行","その他","自分の銀行へ移動","月末資金移動","0","7zq7tTseJM7jlAqBjObvGGz4_EW5B7uZZ-k_4ezkH_c"
|
||||||
|
"0","2026/02/27","ラクテンカ-ドサ-ビス","-1986","楽天銀行","現金・カード","カード引き落とし","","1","u8GEOP01cAIupk_khqDWr4pX2LaFk-AgH2LmgaVZ6c4"
|
||||||
|
"1","2026/02/27","カ)ブル-スト-ンリンクアンドサ-クル","542588","楽天銀行","収入","給与","ボーナス","0","it3TfQeYTZTBRn7Ks_dJlXIliwz45QASdt6A4EOpvBo"
|
||||||
|
"1","2026/02/26","日新火災保険料(A)","-5000","三井住友カード (VpassID)","住宅","地震・火災保険","火災保険","0","o7iFvqBM1AMSbdLF1KHvTHiHAYT5wqGrxnlUxs6FP3E"
|
||||||
|
"1","2026/02/26","振込*ナカシマ ケイコ","-50000","住信SBIネット銀行","その他","恵子の振込み","恵子さんの振り込み","0","cpUMwAEmZXhX2SXRwck-DfI69VvzhqedXI7thvaGJPQ"
|
||||||
|
"1","2026/02/26","普通 円 貯金用口座","-30000","住信SBIネット銀行","その他","天引き貯金","天引き貯金","0","zTcQHDJLKeyZjfw_J6L8IejwOm3us8CUlBDuK5m0g0Q"
|
||||||
|
"0","2026/02/26","口座振替 ミツイスミトモカード","-132096","住信SBIネット銀行","現金・カード","カード引き落とし","","1","-XbWI4qyXlg0q1VwoDiQfD_YSRzTx2KWZp1SxuVJRS8"
|
||||||
|
"0","2026/02/26","ミツイスミトモカ-ド (カ","-106680","三井住友銀行(Olive)","現金・カード","カード引き落とし","","1","Ut6QWHF9bZ3tqKAcO8BinjC3fEjJX1DMRtDjxmr47Tc"
|
||||||
|
"0","2026/02/25","入 座間 出 小 新宿","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","AwJqizS_gtG592HBUNiiGoyQU6xNOQPNLZly64dknss"
|
||||||
|
"0","2026/02/25","入 小 新宿 出 座間","-472","モバイルSuica","交通費","通勤電車","通勤電車","0","NcIcGTgtAkiU-Jx_IforJyb65Dw4gwrPwoseWVEJZoI"
|
||||||
|
"1","2026/02/25","京王モ-ル アネックス/NFC","-280","三井住友カード (VpassID)","食費","カフェ","ドトール","0","yde4sobxVJBz6itXIBLI9JDHvGg-7DFGzk3z4Hn0GMQ"
|
||||||
|
"1","2026/02/25","株式会社エネライフ","-6529","三井住友カード (VpassID)","水道・光熱費","ガス・灯油代","ガス代","0","5xi2jSIMU_AegMdDtgB6luxNfTtFxHbCtL2JcU5Yz4Q"
|
||||||
|
"1","2026/02/25","給与 カ)ブル-スト-ンリンクアンドサ-クル","308904","楽天銀行","収入","給与","給料","0","b-WrLkHnkvCeraJAGwsT7f9yqJBAi5yWZVdbvPZebIo"
|
||||||
|
@@ -0,0 +1,7 @@
|
|||||||
|
"計算対象","日付","内容","金額(円)","保有金融機関","大項目","中項目","メモ","振替","ID"
|
||||||
|
"1","2026/03/26","振込*ナカシマ ケイコ","-50000","住信SBIネット銀行","その他","恵子の振込み","","0","dsBS_J-Jd3ON9DSiWa651NhaPkXLaVJ-alHM4aVu7zM"
|
||||||
|
"0","2026/03/26","ミツイスミトモカ-ド (カ","-103251","三井住友銀行(Olive)","現金・カード","カード引き落とし","","1","LMNZAzcoshROC4fGgW5eHxAk8w075UiC_UIVk_lOpJI"
|
||||||
|
"1","2026/03/26","普通 円 貯金用口座","-30000","住信SBIネット銀行","その他","天引き貯金","","0","JmBcff8FCu_VzQnyG2_jm2om_dpcE9Hw4LvyJ807ax8"
|
||||||
|
"0","2026/03/26","口座振替 ミツイスミトモカード","-376947","住信SBIネット銀行","現金・カード","カード引き落とし","","1","QoXGwc-bOs681N6muH3iUwy2F8cNSzHbCMjI6GtR-2c"
|
||||||
|
"1","2026/03/25","株式会社エネライフ","-6424","三井住友カード (VpassID)","水道・光熱費","ガス・灯油代","","0","mkv3RRTM1WyEtXAl-pdYzm4B1-2kD3RI_BAHQttFiKs"
|
||||||
|
"1","2026/03/25","給与 カ)ブル-スト-ンリンクアンドサ-クル","310511","楽天銀行","収入","給与","","0","vj7Lrfxd655LT5kOD089T8IpMt1sv3wkPTqdfEXtTeQ"
|
||||||
|
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -0,0 +1,87 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// For Node.js environments that support TextDecoder
|
||||||
|
// Using iconv-lite or manual Shift-JIS decoding
|
||||||
|
const sjis = require('encoding');
|
||||||
|
|
||||||
|
const csvDir = './MoneyForwardエクスポート';
|
||||||
|
|
||||||
|
const files = fs.readdirSync(csvDir).filter(f => f.endsWith('.csv'));
|
||||||
|
|
||||||
|
const categories = new Map(); // key: "大項目|中項目", value: {institutions: Set, hasNonTransfer: boolean}
|
||||||
|
const institutions = new Set();
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const filePath = path.join(csvDir, file);
|
||||||
|
const buffer = fs.readFileSync(filePath);
|
||||||
|
|
||||||
|
// Try to decode from Shift-JIS
|
||||||
|
let content;
|
||||||
|
try {
|
||||||
|
// Using Node.js built-in Buffer (assumes UTF-8, may fail)
|
||||||
|
content = buffer.toString('utf8');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Could not decode file: ${file}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
// Parse header
|
||||||
|
const headerLine = lines[0];
|
||||||
|
const headers = headerLine.split(',');
|
||||||
|
|
||||||
|
const indices = {
|
||||||
|
大項目: headers.indexOf('大項目'),
|
||||||
|
中項目: headers.indexOf('中項目'),
|
||||||
|
保有金融機関: headers.indexOf('保有金融機関'),
|
||||||
|
振替: headers.indexOf('振替')
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`File: ${file}`);
|
||||||
|
console.log(`Headers found:`, indices);
|
||||||
|
|
||||||
|
// Parse data rows
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
const fields = line.split(',');
|
||||||
|
if (fields.length < Math.max(...Object.values(indices)) + 1) continue;
|
||||||
|
|
||||||
|
const category = fields[indices.大項目];
|
||||||
|
const subCategory = fields[indices.中項目];
|
||||||
|
const institution = fields[indices.保有金融機関];
|
||||||
|
const transfer = fields[indices.振替];
|
||||||
|
|
||||||
|
if (!category || !subCategory) continue;
|
||||||
|
|
||||||
|
const key = `${category}|${subCategory}`;
|
||||||
|
|
||||||
|
if (!categories.has(key)) {
|
||||||
|
categories.set(key, { institutions: new Set(), hasNonTransfer: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
categories.get(key).institutions.add(institution);
|
||||||
|
if (transfer === '0') {
|
||||||
|
categories.get(key).hasNonTransfer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
institutions.add(institution);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n=== UNIQUE CATEGORIES ===\n');
|
||||||
|
console.log('大項目 | 中項目 | Institutions | Has Non-Transfer');
|
||||||
|
console.log('-'.repeat(80));
|
||||||
|
|
||||||
|
const sortedCategories = Array.from(categories.entries()).sort();
|
||||||
|
sortedCategories.forEach(([key, data]) => {
|
||||||
|
const [大項目, 中項目] = key.split('|');
|
||||||
|
const instList = Array.from(data.institutions).join('; ');
|
||||||
|
console.log(`${大項目} | ${中項目} | ${instList} | ${data.hasNonTransfer ? 'Yes' : 'No'}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n=== INSTITUTIONS ===\n');
|
||||||
|
Array.from(institutions).sort().forEach(inst => console.log(inst));
|
||||||
|
After Width: | Height: | Size: 53 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ja">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>家計簿 - 複式簿記</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 65 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "household-bookkeeping",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"date-fns": "^3.3.1",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"recharts": "^2.10.3",
|
||||||
|
"zustand": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/papaparse": "^5.3.14",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useStore } from './store';
|
||||||
|
import Layout from './components/Layout';
|
||||||
|
import Dashboard from './components/Dashboard';
|
||||||
|
import ImportPage from './components/ImportPage';
|
||||||
|
import JournalPage from './components/JournalPage';
|
||||||
|
import LedgerPage from './components/LedgerPage';
|
||||||
|
import BalanceSheetPage from './components/BalanceSheetPage';
|
||||||
|
import SettingsPage from './components/SettingsPage';
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const { currentPage } = useStore();
|
||||||
|
|
||||||
|
const renderPage = () => {
|
||||||
|
switch (currentPage) {
|
||||||
|
case 'dashboard': return <Dashboard />;
|
||||||
|
case 'import': return <ImportPage />;
|
||||||
|
case 'journal': return <JournalPage />;
|
||||||
|
case 'ledger': return <LedgerPage />;
|
||||||
|
case 'balancesheet': return <BalanceSheetPage />;
|
||||||
|
case 'settings': return <SettingsPage />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
{renderPage()}
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,409 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { useStore } from '../store';
|
||||||
|
import { buildBalanceSheet, getAvailableMonths, formatAmount, filterByMonth, calcPL } from '../utils/bookkeeping';
|
||||||
|
|
||||||
|
type Tab = 'bs' | 'pl';
|
||||||
|
|
||||||
|
export default function BalanceSheetPage() {
|
||||||
|
const {
|
||||||
|
transactions, accounts, openingBalances, upsertOpeningBalance,
|
||||||
|
selectedMonth: targetMonth, setSelectedMonth: setTargetMonth,
|
||||||
|
navigateTo, setSelectedLedgerAccountId,
|
||||||
|
balancesheetTab: tab, setBalancesheetTab: setTab,
|
||||||
|
} = useStore();
|
||||||
|
|
||||||
|
const availableMonths = useMemo(() => getAvailableMonths(transactions), [transactions]);
|
||||||
|
const [showOpeningEditor, setShowOpeningEditor] = useState(false);
|
||||||
|
const [obInput, setObInput] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
// 対象月末日(BS用)
|
||||||
|
const targetDate = useMemo(() => {
|
||||||
|
if (!targetMonth) return undefined;
|
||||||
|
const [y, m] = targetMonth.split('-').map(Number);
|
||||||
|
const lastDay = new Date(y, m, 0).getDate();
|
||||||
|
return `${targetMonth}-${String(lastDay).padStart(2, '0')}`;
|
||||||
|
}, [targetMonth]);
|
||||||
|
|
||||||
|
const bs = useMemo(
|
||||||
|
() => buildBalanceSheet(accounts, transactions, openingBalances, targetDate),
|
||||||
|
[accounts, transactions, openingBalances, targetDate]
|
||||||
|
);
|
||||||
|
|
||||||
|
// P&L: 対象月 or 全期間
|
||||||
|
const plTransactions = useMemo(() => {
|
||||||
|
if (!targetMonth) return transactions;
|
||||||
|
const [y, m] = targetMonth.split('-').map(Number);
|
||||||
|
return filterByMonth(transactions, y, m);
|
||||||
|
}, [transactions, targetMonth]);
|
||||||
|
|
||||||
|
const pl = useMemo(() => calcPL(accounts, plTransactions), [accounts, plTransactions]);
|
||||||
|
|
||||||
|
const handleSaveOpeningBalances = () => {
|
||||||
|
for (const [accountId, raw] of Object.entries(obInput)) {
|
||||||
|
const amount = parseInt(raw.replace(/,/g, ''), 10);
|
||||||
|
if (!isNaN(amount)) upsertOpeningBalance({ accountId, amount });
|
||||||
|
}
|
||||||
|
setShowOpeningEditor(false);
|
||||||
|
setObInput({});
|
||||||
|
};
|
||||||
|
|
||||||
|
const assetAccounts = accounts.filter(a => a.type === 'asset');
|
||||||
|
|
||||||
|
// P&L用: 収益・費用の勘定科目リスト(残高ありのみ)
|
||||||
|
const incomeItems = accounts
|
||||||
|
.filter(a => a.type === 'income')
|
||||||
|
.map(a => ({ account: a, amount: pl.byAccount.get(a.id) ?? 0 }))
|
||||||
|
.filter(d => d.amount !== 0)
|
||||||
|
.sort((a, b) => b.amount - a.amount);
|
||||||
|
|
||||||
|
const expenseItems = accounts
|
||||||
|
.filter(a => a.type === 'expense')
|
||||||
|
.map(a => ({ account: a, amount: pl.byAccount.get(a.id) ?? 0 }))
|
||||||
|
.filter(d => d.amount !== 0)
|
||||||
|
.sort((a, b) => b.amount - a.amount);
|
||||||
|
|
||||||
|
const periodLabel = targetMonth
|
||||||
|
? `${targetMonth.replace('-', '年')}月`
|
||||||
|
: '全期間';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* ヘッダー */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-bold text-slate-100">財務諸表</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{tab === 'bs' && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowOpeningEditor(!showOpeningEditor)}
|
||||||
|
className="px-3 py-1.5 text-sm border border-slate-600 text-slate-300 rounded-md hover:bg-slate-700"
|
||||||
|
>
|
||||||
|
期首残高を設定
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<select
|
||||||
|
value={targetMonth}
|
||||||
|
onChange={e => setTargetMonth(e.target.value)}
|
||||||
|
className="border rounded-md px-3 py-1.5 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">全期間</option>
|
||||||
|
{availableMonths.map(m => (
|
||||||
|
<option key={m} value={m}>{m.replace('-', '年')}月</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* タブ */}
|
||||||
|
<div className="flex gap-1 border-b border-slate-700">
|
||||||
|
{([['bs', '貸借対照表(B/S)'], ['pl', '損益計算書(P/L)']] as [Tab, string][]).map(([t, label]) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
onClick={() => setTab(t)}
|
||||||
|
className={`px-5 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
tab === t
|
||||||
|
? 'border-indigo-500 text-indigo-400'
|
||||||
|
: 'border-transparent text-slate-500 hover:text-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 期首残高編集 */}
|
||||||
|
{showOpeningEditor && tab === 'bs' && (
|
||||||
|
<div className="card border-amber-700 bg-amber-900/20">
|
||||||
|
<h3 className="text-sm font-semibold text-amber-300 mb-3">
|
||||||
|
期首残高の設定(インポートCSVの開始前の残高)
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
{assetAccounts.map(acc => {
|
||||||
|
const current = openingBalances.find(b => b.accountId === acc.id)?.amount ?? 0;
|
||||||
|
return (
|
||||||
|
<div key={acc.id} className="flex items-center gap-2">
|
||||||
|
<label className="text-xs text-slate-400 w-28 shrink-0">{acc.name}</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
defaultValue={current}
|
||||||
|
onChange={e => setObInput(prev => ({ ...prev, [acc.id]: e.target.value }))}
|
||||||
|
className="border rounded px-2 py-1 text-sm w-32"
|
||||||
|
placeholder="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={handleSaveOpeningBalances}
|
||||||
|
className="px-4 py-1.5 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowOpeningEditor(false)}
|
||||||
|
className="px-4 py-1.5 border border-slate-600 text-slate-300 text-sm rounded-md hover:bg-slate-700"
|
||||||
|
>
|
||||||
|
キャンセル
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ===== 貸借対照表 ===== */}
|
||||||
|
{tab === 'bs' && (
|
||||||
|
<>
|
||||||
|
{/* 期首残高未設定の警告 */}
|
||||||
|
{openingBalances.length === 0 && transactions.length > 0 && (
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
※ 期首残高未設定のため資産残高が不正確な場合があります。
|
||||||
|
<button onClick={() => setShowOpeningEditor(true)} className="ml-1 underline hover:text-slate-400">設定する</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{/* 左: 資産 */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-bold text-slate-400 border-b border-slate-700 pb-2 mb-3">
|
||||||
|
資産の部
|
||||||
|
</h3>
|
||||||
|
<table className="w-full">
|
||||||
|
<tbody>
|
||||||
|
{bs.assets.map(({ account, balance }) => (
|
||||||
|
<tr key={account.id} className="border-b border-slate-700/50">
|
||||||
|
<td className="py-1.5 text-sm text-slate-300">{account.name}</td>
|
||||||
|
<td className="py-1.5 text-sm font-medium text-right text-slate-200">
|
||||||
|
{formatAmount(balance)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="border-t-2 border-slate-600">
|
||||||
|
<td className="pt-2 text-sm font-bold text-slate-200">資産合計</td>
|
||||||
|
<td className="pt-2 text-sm font-bold text-right text-indigo-400">
|
||||||
|
{formatAmount(bs.totalAssets)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右: 負債 + 純資産 */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-bold text-slate-400 border-b border-slate-700 pb-2 mb-3">
|
||||||
|
負債の部
|
||||||
|
</h3>
|
||||||
|
<table className="w-full">
|
||||||
|
<tbody>
|
||||||
|
{bs.liabilities.length === 0 ? (
|
||||||
|
<tr><td colSpan={2} className="text-sm text-slate-500 py-2">負債なし</td></tr>
|
||||||
|
) : (
|
||||||
|
bs.liabilities.map(({ account, balance }) => (
|
||||||
|
<tr key={account.id} className="border-b border-slate-700/50">
|
||||||
|
<td className="py-1.5 text-sm text-slate-300">{account.name}</td>
|
||||||
|
<td className="py-1.5 text-sm font-medium text-right text-slate-200">
|
||||||
|
{formatAmount(balance)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="border-t-2 border-slate-600">
|
||||||
|
<td className="pt-2 text-sm font-bold text-slate-200">負債合計</td>
|
||||||
|
<td className="pt-2 text-sm font-bold text-right text-rose-400">
|
||||||
|
{formatAmount(bs.totalLiabilities)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-bold text-slate-400 border-b border-slate-700 pb-2 mb-3">
|
||||||
|
純資産の部
|
||||||
|
</h3>
|
||||||
|
<table className="w-full">
|
||||||
|
<tbody>
|
||||||
|
{bs.equity.map(({ account, balance }) => (
|
||||||
|
<tr key={account.id} className="border-b border-slate-700/50">
|
||||||
|
<td className="py-1.5 text-sm text-slate-300">{account.name}</td>
|
||||||
|
<td className={`py-1.5 text-sm font-medium text-right ${balance >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||||
|
{formatAmount(balance, true)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="border-t-2 border-slate-600">
|
||||||
|
<td className="pt-2 text-sm font-bold text-slate-200">純資産合計</td>
|
||||||
|
<td className={`pt-2 text-sm font-bold text-right ${bs.totalEquity >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||||
|
{formatAmount(bs.totalEquity, true)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-slate-700/50">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="font-bold text-slate-300">負債 + 純資産 合計</span>
|
||||||
|
<span className="font-bold text-slate-100">
|
||||||
|
{formatAmount(bs.totalLiabilities + bs.totalEquity)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{Math.abs(bs.totalAssets - (bs.totalLiabilities + bs.totalEquity)) < 1 ? (
|
||||||
|
<p className="text-xs text-emerald-400 mt-1">✅ 貸借一致</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-amber-400 mt-1">
|
||||||
|
⚠️ 差額: {formatAmount(bs.totalAssets - bs.totalLiabilities - bs.totalEquity)}
|
||||||
|
(期首残高を設定すると一致します)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 純資産サマリ */}
|
||||||
|
<div className="card border-2 border-indigo-700 bg-indigo-950/40">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold text-indigo-300">純資産(資産 − 負債)</p>
|
||||||
|
<p className="text-xs text-indigo-500 mt-0.5">
|
||||||
|
{targetMonth ? `${periodLabel}末時点` : '現時点'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className={`text-3xl font-bold ${bs.netWorth >= 0 ? 'text-indigo-300' : 'text-rose-400'}`}>
|
||||||
|
{formatAmount(bs.netWorth)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ===== 損益計算書 ===== */}
|
||||||
|
{tab === 'pl' && (
|
||||||
|
<>
|
||||||
|
<p className="text-sm text-slate-400">対象期間: <strong className="text-slate-200">{periodLabel}</strong></p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
{/* 費用の部 */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-bold text-rose-400 border-b border-rose-900 pb-2 mb-3">
|
||||||
|
費用の部
|
||||||
|
</h3>
|
||||||
|
{expenseItems.length === 0 ? (
|
||||||
|
<p className="text-sm text-slate-500">費用なし</p>
|
||||||
|
) : (
|
||||||
|
<table className="w-full">
|
||||||
|
<tbody>
|
||||||
|
{expenseItems.map(({ account, amount }) => {
|
||||||
|
const pct = pl.expenseTotal > 0 ? (amount / pl.expenseTotal * 100) : 0;
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={account.id}
|
||||||
|
className="border-b border-slate-700/50 hover:bg-slate-700/50 cursor-pointer"
|
||||||
|
onClick={() => { setSelectedLedgerAccountId(account.id); navigateTo('ledger'); }}
|
||||||
|
>
|
||||||
|
<td className="py-1.5 text-sm text-slate-300 hover:text-indigo-300">{account.name}</td>
|
||||||
|
<td className="py-1.5 text-sm text-right">
|
||||||
|
<span className="font-medium text-rose-400">{formatAmount(amount)}</span>
|
||||||
|
<span className="text-xs text-slate-500 ml-2">{pct.toFixed(1)}%</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="border-t-2 border-rose-900">
|
||||||
|
<td className="pt-2 text-sm font-bold text-slate-200">費用合計</td>
|
||||||
|
<td className="pt-2 text-sm font-bold text-right text-rose-400">
|
||||||
|
{formatAmount(pl.expenseTotal)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 収益の部 */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-bold text-emerald-400 border-b border-emerald-900 pb-2 mb-3">
|
||||||
|
収益の部
|
||||||
|
</h3>
|
||||||
|
{incomeItems.length === 0 ? (
|
||||||
|
<p className="text-sm text-slate-500">収益なし</p>
|
||||||
|
) : (
|
||||||
|
<table className="w-full">
|
||||||
|
<tbody>
|
||||||
|
{incomeItems.map(({ account, amount }) => (
|
||||||
|
<tr
|
||||||
|
key={account.id}
|
||||||
|
className="border-b border-slate-700/50 hover:bg-slate-700/50 cursor-pointer"
|
||||||
|
onClick={() => { setSelectedLedgerAccountId(account.id); navigateTo('ledger'); }}
|
||||||
|
>
|
||||||
|
<td className="py-1.5 text-sm text-slate-300 hover:text-indigo-300">{account.name}</td>
|
||||||
|
<td className="py-1.5 text-sm font-medium text-right text-emerald-400">
|
||||||
|
{formatAmount(amount)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr className="border-t-2 border-emerald-900">
|
||||||
|
<td className="pt-2 text-sm font-bold text-slate-200">収益合計</td>
|
||||||
|
<td className="pt-2 text-sm font-bold text-right text-emerald-400">
|
||||||
|
{formatAmount(pl.incomeTotal)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 当期純損益 */}
|
||||||
|
<div className={`card border-2 ${pl.netIncome >= 0 ? 'border-emerald-800 bg-emerald-950/40' : 'border-rose-800 bg-rose-950/40'}`}>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
<p className={`text-sm font-semibold ${pl.netIncome >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||||
|
当期純{pl.netIncome >= 0 ? '利益' : '損失'}
|
||||||
|
<span className="text-xs font-normal ml-2 opacity-70">収益合計 − 費用合計</span>
|
||||||
|
</p>
|
||||||
|
<p className={`text-xs mt-0.5 opacity-60 ${pl.netIncome >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||||
|
{periodLabel}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className={`text-3xl font-bold ${pl.netIncome >= 0 ? 'text-emerald-400' : 'text-rose-400'}`}>
|
||||||
|
{formatAmount(pl.netIncome, true)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 収益・費用の比率バー */}
|
||||||
|
{(pl.incomeTotal + pl.expenseTotal) > 0 && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<div className="flex h-3 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="bg-emerald-500"
|
||||||
|
style={{ width: `${(pl.incomeTotal / (pl.incomeTotal + pl.expenseTotal)) * 100}%` }}
|
||||||
|
/>
|
||||||
|
<div className="bg-rose-500 flex-1" />
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-xs mt-1 opacity-70">
|
||||||
|
<span className={pl.netIncome >= 0 ? 'text-emerald-400' : 'text-rose-400'}>
|
||||||
|
収益 {formatAmount(pl.incomeTotal)}
|
||||||
|
</span>
|
||||||
|
<span className={pl.netIncome >= 0 ? 'text-emerald-400' : 'text-rose-400'}>
|
||||||
|
費用 {formatAmount(pl.expenseTotal)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell, PieChart, Pie,
|
||||||
|
} from 'recharts';
|
||||||
|
import { useStore } from '../store';
|
||||||
|
import { calcMonthlyPL, getAvailableMonths, formatAmount, filterByMonth } from '../utils/bookkeeping';
|
||||||
|
|
||||||
|
const EXPENSE_COLORS = [
|
||||||
|
'#6366f1','#8b5cf6','#ec4899','#f43f5e','#f97316',
|
||||||
|
'#eab308','#84cc16','#22c55e','#14b8a6','#06b6d4',
|
||||||
|
];
|
||||||
|
|
||||||
|
const CHART_TOOLTIP_STYLE = {
|
||||||
|
backgroundColor: '#475569',
|
||||||
|
border: '1px solid #64748b',
|
||||||
|
color: '#f1f5f9',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Dashboard() {
|
||||||
|
const { transactions, accounts, selectedMonth, setSelectedMonth, navigateTo, setSelectedLedgerAccountId, setBalancesheetTab } = useStore();
|
||||||
|
|
||||||
|
const availableMonths = useMemo(() => getAvailableMonths(transactions), [transactions]);
|
||||||
|
const [drillAccountId, setDrillAccountId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// 初回: selectedMonthが未設定なら最新月をセット
|
||||||
|
const effectiveMonth = selectedMonth || availableMonths[0] || '';
|
||||||
|
|
||||||
|
const [year, month] = effectiveMonth
|
||||||
|
? [parseInt(effectiveMonth.split('-')[0]), parseInt(effectiveMonth.split('-')[1])]
|
||||||
|
: [new Date().getFullYear(), new Date().getMonth() + 1];
|
||||||
|
|
||||||
|
const pl = useMemo(
|
||||||
|
() => calcMonthlyPL(accounts, transactions, year, month),
|
||||||
|
[accounts, transactions, year, month]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 費用の内訳(円グラフ用)- accountIdも保持
|
||||||
|
const expenseBreakdown = useMemo(() => {
|
||||||
|
return accounts
|
||||||
|
.filter(a => a.type === 'expense')
|
||||||
|
.map(a => ({ id: a.id, name: a.name, value: pl.byAccount.get(a.id) ?? 0 }))
|
||||||
|
.filter(d => d.value > 0)
|
||||||
|
.sort((a, b) => b.value - a.value)
|
||||||
|
.slice(0, 10);
|
||||||
|
}, [accounts, pl]);
|
||||||
|
|
||||||
|
// 月別収支の棒グラフデータ(直近6ヶ月)
|
||||||
|
const monthlyData = useMemo(() => {
|
||||||
|
const months = availableMonths.slice(0, 6).reverse();
|
||||||
|
const hasMultipleYears = new Set(months.map(m => m.split('-')[0])).size > 1;
|
||||||
|
return months.map((m, i) => {
|
||||||
|
const [y, mo] = m.split('-').map(Number);
|
||||||
|
const prevYear = i > 0 ? parseInt(months[i - 1].split('-')[0]) : null;
|
||||||
|
const showYear = hasMultipleYears && (i === 0 || y !== prevYear);
|
||||||
|
const { incomeTotal, expenseTotal } = calcMonthlyPL(accounts, transactions, y, mo);
|
||||||
|
return {
|
||||||
|
month: showYear ? `'${String(y).slice(2)}/${mo}月` : `${mo}月`,
|
||||||
|
収入: incomeTotal,
|
||||||
|
支出: expenseTotal,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [accounts, transactions, availableMonths]);
|
||||||
|
|
||||||
|
|
||||||
|
// ドリルダウン: 選択勘定のMFカテゴリ別内訳
|
||||||
|
const drillData = useMemo(() => {
|
||||||
|
if (!drillAccountId) return [];
|
||||||
|
const monthTxAll = filterByMonth(transactions, year, month);
|
||||||
|
const groups = new Map<string, { total: number; items: { date: string; description: string; amount: number }[] }>();
|
||||||
|
|
||||||
|
for (const tx of monthTxAll) {
|
||||||
|
const entry = tx.entries.find(e => e.accountId === drillAccountId && e.debit > 0);
|
||||||
|
if (!entry) continue;
|
||||||
|
const key = [tx.mfCategory, tx.mfSubCategory].filter(Boolean).join(' / ') || '(未分類)';
|
||||||
|
if (!groups.has(key)) groups.set(key, { total: 0, items: [] });
|
||||||
|
const g = groups.get(key)!;
|
||||||
|
g.total += entry.debit;
|
||||||
|
g.items.push({ date: tx.date, description: tx.description, amount: entry.debit });
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...groups.entries()]
|
||||||
|
.map(([label, { total, items }]) => ({ label, total, items: items.sort((a, b) => b.amount - a.amount) }))
|
||||||
|
.sort((a, b) => b.total - a.total);
|
||||||
|
}, [drillAccountId, transactions, year, month]);
|
||||||
|
|
||||||
|
const drillAccount = accounts.find(a => a.id === drillAccountId);
|
||||||
|
|
||||||
|
if (transactions.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-slate-500">
|
||||||
|
<p className="text-5xl mb-4">📂</p>
|
||||||
|
<p className="text-lg font-medium text-slate-400">データがありません</p>
|
||||||
|
<p className="text-sm mt-1">「CSVインポート」からMoneyForwardのCSVを読み込んでください</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
{/* ヘッダー */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-bold text-slate-100">ダッシュボード</h2>
|
||||||
|
<select
|
||||||
|
value={effectiveMonth}
|
||||||
|
onChange={e => { setSelectedMonth(e.target.value); setDrillAccountId(null); }}
|
||||||
|
className="border rounded-md px-3 py-1.5 text-sm"
|
||||||
|
>
|
||||||
|
{availableMonths.map(m => (
|
||||||
|
<option key={m} value={m}>{m.replace('-', '年')}月</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* サマリカード */}
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div
|
||||||
|
className="card cursor-pointer hover:ring-1 hover:ring-indigo-500 transition-all"
|
||||||
|
onClick={() => { setBalancesheetTab('pl'); navigateTo('balancesheet'); }}
|
||||||
|
title="損益計算書で見る"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-slate-400 mb-1">収入合計</p>
|
||||||
|
<p className="text-2xl font-bold amount-positive">{formatAmount(pl.incomeTotal)}</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="card cursor-pointer hover:ring-1 hover:ring-indigo-500 transition-all"
|
||||||
|
onClick={() => { setBalancesheetTab('pl'); navigateTo('balancesheet'); }}
|
||||||
|
title="損益計算書で見る"
|
||||||
|
>
|
||||||
|
<p className="text-xs text-slate-400 mb-1">支出合計</p>
|
||||||
|
<p className="text-2xl font-bold amount-negative">{formatAmount(pl.expenseTotal)}</p>
|
||||||
|
</div>
|
||||||
|
<div className="card">
|
||||||
|
<p className="text-xs text-slate-400 mb-1">収支(黒字/赤字)</p>
|
||||||
|
<p className={`text-2xl font-bold ${pl.netIncome >= 0 ? 'amount-positive' : 'amount-negative'}`}>
|
||||||
|
{formatAmount(pl.netIncome, true)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* グラフ */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{/* 月別収支棒グラフ */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-3">月別収支</h3>
|
||||||
|
<ResponsiveContainer width="100%" height={200}>
|
||||||
|
<BarChart data={monthlyData} margin={{ top: 0, right: 10, left: 10, bottom: 0 }}>
|
||||||
|
<XAxis dataKey="month" tick={{ fontSize: 12, fill: '#94a3b8' }} />
|
||||||
|
<YAxis tickFormatter={v => `${(v / 10000).toFixed(0)}万`} tick={{ fontSize: 11, fill: '#94a3b8' }} />
|
||||||
|
<Tooltip
|
||||||
|
formatter={(v: number) => formatAmount(v)}
|
||||||
|
contentStyle={CHART_TOOLTIP_STYLE}
|
||||||
|
/>
|
||||||
|
<Bar dataKey="収入" fill="#22c55e" radius={[3,3,0,0]} />
|
||||||
|
<Bar dataKey="支出" fill="#f43f5e" radius={[3,3,0,0]} />
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 費用内訳円グラフ */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-2">支出内訳</h3>
|
||||||
|
{expenseBreakdown.length > 0 ? (
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{/* 円グラフ(視覚のみ) */}
|
||||||
|
<div className="shrink-0">
|
||||||
|
<ResponsiveContainer width={150} height={180}>
|
||||||
|
<PieChart>
|
||||||
|
<Pie
|
||||||
|
data={expenseBreakdown}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
outerRadius={70}
|
||||||
|
innerRadius={28}
|
||||||
|
label={false}
|
||||||
|
cursor="pointer"
|
||||||
|
onClick={(data: { id: string }) => {
|
||||||
|
setDrillAccountId(prev => prev === data.id ? null : data.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{expenseBreakdown.map((item, i) => (
|
||||||
|
<Cell
|
||||||
|
key={i}
|
||||||
|
fill={EXPENSE_COLORS[i % EXPENSE_COLORS.length]}
|
||||||
|
opacity={drillAccountId && drillAccountId !== item.id ? 0.35 : 1}
|
||||||
|
stroke={drillAccountId === item.id ? '#a5b4fc' : '#1e293b'}
|
||||||
|
strokeWidth={drillAccountId === item.id ? 2 : 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip
|
||||||
|
formatter={(v: number) => formatAmount(v)}
|
||||||
|
contentStyle={CHART_TOOLTIP_STYLE}
|
||||||
|
/>
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* クリッカブルな凡例リスト */}
|
||||||
|
<div className="flex-1 flex flex-col gap-1 overflow-y-auto" style={{ maxHeight: 180 }}>
|
||||||
|
{expenseBreakdown.map((item, i) => {
|
||||||
|
const pct = pl.expenseTotal > 0 ? (item.value / pl.expenseTotal * 100) : 0;
|
||||||
|
const isSelected = drillAccountId === item.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => setDrillAccountId(prev => prev === item.id ? null : item.id)}
|
||||||
|
className={`flex items-center gap-2 text-left w-full px-2 py-1 rounded transition-colors ${
|
||||||
|
isSelected ? 'bg-indigo-900/50 ring-1 ring-indigo-500' : 'hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="shrink-0 w-2.5 h-2.5 rounded-sm"
|
||||||
|
style={{ backgroundColor: EXPENSE_COLORS[i % EXPENSE_COLORS.length] }}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-300 flex-1 truncate">{item.name}</span>
|
||||||
|
<span className="text-xs text-slate-500 shrink-0">{pct.toFixed(0)}%</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-slate-500 text-sm">支出データなし</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ドリルダウンパネル */}
|
||||||
|
{drillAccountId && drillAccount && (
|
||||||
|
<div className="card border-indigo-700 bg-indigo-950/40">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-bold text-indigo-300">
|
||||||
|
{drillAccount.name} の内訳
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-indigo-500">
|
||||||
|
合計: {formatAmount(pl.byAccount.get(drillAccountId) ?? 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setDrillAccountId(null)}
|
||||||
|
className="text-indigo-400 hover:text-indigo-200 text-lg leading-none"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{drillData.map(group => (
|
||||||
|
<details key={group.label} className="bg-slate-800 rounded-lg overflow-hidden border border-slate-700">
|
||||||
|
<summary className="flex items-center justify-between px-3 py-2 cursor-pointer hover:bg-slate-700">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-slate-500">▶</span>
|
||||||
|
<span className="text-sm font-medium text-slate-200">{group.label}</span>
|
||||||
|
<span className="text-xs text-slate-400 bg-slate-700 px-1.5 py-0.5 rounded-full">
|
||||||
|
{group.items.length}件
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-semibold text-rose-400">{formatAmount(group.total)}</span>
|
||||||
|
</summary>
|
||||||
|
<div className="border-t border-slate-700">
|
||||||
|
<table className="w-full">
|
||||||
|
<tbody>
|
||||||
|
{group.items.map((item, i) => (
|
||||||
|
<tr key={i} className="border-b border-slate-700/50 last:border-0">
|
||||||
|
<td className="px-3 py-1.5 text-xs text-slate-500 w-24">{item.date}</td>
|
||||||
|
<td className="px-3 py-1.5 text-sm text-slate-300">{item.description}</td>
|
||||||
|
<td className="px-3 py-1.5 text-sm font-medium text-rose-400 text-right w-28">
|
||||||
|
{formatAmount(item.amount)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 費用ランキング */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-3">費目別支出</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{expenseBreakdown.map((item, i) => {
|
||||||
|
const pct = pl.expenseTotal > 0 ? (item.value / pl.expenseTotal) * 100 : 0;
|
||||||
|
const isSelected = drillAccountId === item.id;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.name}
|
||||||
|
onClick={() => setDrillAccountId(prev => prev === item.id ? null : item.id)}
|
||||||
|
className={`flex items-center gap-2 rounded-md px-2 py-1 cursor-pointer transition-colors ${
|
||||||
|
isSelected ? 'bg-indigo-900/50 ring-1 ring-indigo-500' : 'hover:bg-slate-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-xs text-slate-500 w-4">{i + 1}</span>
|
||||||
|
<span className="text-sm text-slate-300 w-32 truncate">{item.name}</span>
|
||||||
|
<div className="flex-1 bg-slate-700 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="h-2 rounded-full"
|
||||||
|
style={{ width: `${pct}%`, backgroundColor: EXPENSE_COLORS[i % EXPENSE_COLORS.length] }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-slate-200 w-24 text-right">
|
||||||
|
{formatAmount(item.value)}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500 w-10 text-right">{pct.toFixed(1)}%</span>
|
||||||
|
<button
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedLedgerAccountId(item.id);
|
||||||
|
navigateTo('ledger');
|
||||||
|
}}
|
||||||
|
className="text-xs text-indigo-400 hover:text-indigo-200 shrink-0 px-1"
|
||||||
|
title="総勘定元帳で見る"
|
||||||
|
>
|
||||||
|
元帳→
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
import { useState, useRef, useMemo } from 'react';
|
||||||
|
import { useStore } from '../store';
|
||||||
|
import { parseMFCsv, convertToTransactions } from '../utils/csvParser';
|
||||||
|
import type { MFRecord } from '../types';
|
||||||
|
|
||||||
|
interface ImportResult {
|
||||||
|
added: number;
|
||||||
|
updated: number;
|
||||||
|
skipped: number;
|
||||||
|
unmapped: MFRecord[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ImportPage() {
|
||||||
|
const {
|
||||||
|
accounts, transactions, categoryMappings, institutionMappings,
|
||||||
|
importedIds, addImportedIds, upsertTransactions,
|
||||||
|
clearTransactions, upsertRawRecords, reapplyMappings, rawRecords,
|
||||||
|
transferOverrides, upsertTransferOverride, removeTransferOverride,
|
||||||
|
} = useStore();
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [dragging, setDragging] = useState(false);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState<ImportResult | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const processFiles = async (files: FileList) => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const idSet = new Set(importedIds);
|
||||||
|
let totalAdded = 0;
|
||||||
|
let totalUpdated = 0;
|
||||||
|
let totalSkipped = 0;
|
||||||
|
const allUnmapped: MFRecord[] = [];
|
||||||
|
|
||||||
|
for (const file of Array.from(files)) {
|
||||||
|
if (!file.name.endsWith('.csv')) continue;
|
||||||
|
const records = await parseMFCsv(file);
|
||||||
|
const { transactions: newTxs, updated, unmapped } = convertToTransactions(
|
||||||
|
records, categoryMappings, institutionMappings, idSet, accounts, transferOverrides
|
||||||
|
);
|
||||||
|
|
||||||
|
const newIds = newTxs.map(t => t.mfId ?? t.id).filter(Boolean);
|
||||||
|
totalUpdated += updated;
|
||||||
|
totalAdded += newTxs.length - updated;
|
||||||
|
totalSkipped += records.length - newTxs.length - unmapped.length;
|
||||||
|
allUnmapped.push(...unmapped);
|
||||||
|
|
||||||
|
upsertTransactions(newTxs);
|
||||||
|
addImportedIds(newIds);
|
||||||
|
upsertRawRecords(records);
|
||||||
|
newIds.forEach(id => idSet.add(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
setResult({ added: totalAdded, updated: totalUpdated, skipped: totalSkipped, unmapped: allUnmapped });
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : String(e));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFiles = (files: FileList | null) => {
|
||||||
|
if (!files || files.length === 0) return;
|
||||||
|
processFiles(files);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setDragging(false);
|
||||||
|
handleFiles(e.dataTransfer.files);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 月一覧(インポート済み)
|
||||||
|
const importedMonths = [...new Set(transactions.map(t => t.date.substring(0, 7)))].sort().reverse();
|
||||||
|
|
||||||
|
// 振替としてスキップされた取引(振替=1 のrawRecords)
|
||||||
|
const skippedTransfers = useMemo(() =>
|
||||||
|
rawRecords.filter(r => r.振替 === '1' && r.ID).sort((a, b) => b.日付.localeCompare(a.日付)),
|
||||||
|
[rawRecords]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 収入/費用勘定科目リスト(振替先の選択用)
|
||||||
|
const incomeExpenseAccounts = accounts.filter(a => a.type === 'income' || a.type === 'expense');
|
||||||
|
|
||||||
|
const [overrideSelects, setOverrideSelects] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<h2 className="text-xl font-bold text-slate-100">CSVインポート</h2>
|
||||||
|
|
||||||
|
{/* ドロップゾーン */}
|
||||||
|
<div
|
||||||
|
onDragOver={e => { e.preventDefault(); setDragging(true); }}
|
||||||
|
onDragLeave={() => setDragging(false)}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
className={`border-2 border-dashed rounded-xl p-12 text-center cursor-pointer transition-colors ${
|
||||||
|
dragging ? 'border-indigo-500 bg-indigo-950/30' : 'border-slate-600 hover:border-indigo-500 hover:bg-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".csv"
|
||||||
|
multiple
|
||||||
|
className="hidden"
|
||||||
|
onChange={e => handleFiles(e.target.files)}
|
||||||
|
/>
|
||||||
|
<p className="text-4xl mb-3">📂</p>
|
||||||
|
{loading ? (
|
||||||
|
<p className="text-indigo-400 font-medium">読み込み中...</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="text-slate-300 font-medium">MoneyForwardのCSVをここにドロップ</p>
|
||||||
|
<p className="text-slate-500 text-sm mt-1">または クリックして選択(複数可)</p>
|
||||||
|
<p className="text-slate-600 text-xs mt-2">
|
||||||
|
「収入・支出詳細」のCSVファイル(Shift-JIS形式)に対応
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 結果 */}
|
||||||
|
{error && (
|
||||||
|
<div className="bg-rose-950/50 border border-rose-800 rounded-lg p-4">
|
||||||
|
<p className="text-rose-400 font-medium">エラー</p>
|
||||||
|
<p className="text-rose-300 text-sm mt-1">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div className="bg-emerald-950/50 border border-emerald-800 rounded-lg p-4 space-y-2">
|
||||||
|
<p className="text-emerald-400 font-semibold">インポート完了</p>
|
||||||
|
<ul className="text-sm text-emerald-300 space-y-1">
|
||||||
|
<li>✅ 追加: <strong>{result.added}件</strong></li>
|
||||||
|
{result.updated > 0 && (
|
||||||
|
<li>🔄 更新(費目変更): <strong>{result.updated}件</strong></li>
|
||||||
|
)}
|
||||||
|
<li>⏭️ スキップ(振替・資産間移動・金額0): <strong>{result.skipped}件</strong></li>
|
||||||
|
{result.unmapped.length > 0 && (
|
||||||
|
<li className="text-amber-400">
|
||||||
|
⚠️ マッピング未設定: <strong>{result.unmapped.length}件</strong>
|
||||||
|
(「設定・マッピング」で勘定科目を設定してください)
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{result.unmapped.length > 0 && (
|
||||||
|
<details className="mt-2">
|
||||||
|
<summary className="text-sm text-amber-400 cursor-pointer">未マッピングの明細を表示</summary>
|
||||||
|
<div className="mt-2 max-h-48 overflow-y-auto">
|
||||||
|
<table className="w-full text-xs">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-amber-900/40">
|
||||||
|
<th className="px-2 py-1 text-left text-amber-300">日付</th>
|
||||||
|
<th className="px-2 py-1 text-left text-amber-300">内容</th>
|
||||||
|
<th className="px-2 py-1 text-left text-amber-300">大項目</th>
|
||||||
|
<th className="px-2 py-1 text-left text-amber-300">中項目</th>
|
||||||
|
<th className="px-2 py-1 text-right text-amber-300">金額</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{result.unmapped.map((r, i) => (
|
||||||
|
<tr key={i} className="border-t border-amber-900/30 text-slate-300">
|
||||||
|
<td className="px-2 py-1">{r.日付}</td>
|
||||||
|
<td className="px-2 py-1">{r.内容}</td>
|
||||||
|
<td className="px-2 py-1">{r.大項目}</td>
|
||||||
|
<td className="px-2 py-1">{r.中項目}</td>
|
||||||
|
<td className="px-2 py-1 text-right">{r.金額.toLocaleString()}円</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* インポート済みデータ一覧 */}
|
||||||
|
{importedMonths.length > 0 && (
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400">インポート済みデータ</h3>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{rawRecords.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const res = reapplyMappings();
|
||||||
|
setResult({ added: res.added, updated: 0, skipped: 0, unmapped: res.unmapped });
|
||||||
|
}}
|
||||||
|
className="text-xs text-indigo-400 hover:text-indigo-300"
|
||||||
|
>
|
||||||
|
マッピングを再適用
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm('全データを削除しますか?この操作は取り消せません。')) {
|
||||||
|
clearTransactions();
|
||||||
|
setResult(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-xs text-rose-500 hover:text-rose-400"
|
||||||
|
>
|
||||||
|
全データを削除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{importedMonths.map(m => {
|
||||||
|
const count = transactions.filter(t => t.date.startsWith(m)).length;
|
||||||
|
return (
|
||||||
|
<div key={m} className="flex items-center justify-between py-1 border-b border-slate-700/50">
|
||||||
|
<span className="text-sm text-slate-300">
|
||||||
|
{m.replace('-', '年')}月
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500">{count}件</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 mt-2">
|
||||||
|
合計 {transactions.length}件
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 振替スキップ除外設定 */}
|
||||||
|
{skippedTransfers.length > 0 && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-1">振替としてスキップされた取引</h3>
|
||||||
|
<p className="text-xs text-slate-500 mb-3">
|
||||||
|
口座間移動として除外されていますが、特定の取引を収入・費用として扱いたい場合は勘定科目を選択してください。
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1 max-h-64 overflow-y-auto">
|
||||||
|
{skippedTransfers.map(r => {
|
||||||
|
const override = transferOverrides.find(o => o.mfId === r.ID);
|
||||||
|
const selectVal = overrideSelects[r.ID] ?? override?.accountId ?? '';
|
||||||
|
return (
|
||||||
|
<div key={r.ID} className="flex items-center gap-2 py-1 border-b border-slate-700/50">
|
||||||
|
<span className="text-xs text-slate-500 w-24 shrink-0">{r.日付}</span>
|
||||||
|
<span className="text-xs text-slate-300 flex-1 truncate">{r.内容}</span>
|
||||||
|
<span className="text-xs text-slate-500 w-16 text-right shrink-0">
|
||||||
|
{r.金額.toLocaleString()}円
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={selectVal}
|
||||||
|
onChange={e => setOverrideSelects(prev => ({ ...prev, [r.ID]: e.target.value }))}
|
||||||
|
className="border rounded px-1 py-0.5 text-xs w-36 shrink-0"
|
||||||
|
>
|
||||||
|
<option value="">スキップ(振替)</option>
|
||||||
|
{incomeExpenseAccounts.map(a => (
|
||||||
|
<option key={a.id} value={a.id}>{a.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{selectVal && selectVal !== (override?.accountId ?? '') && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
upsertTransferOverride({ mfId: r.ID, description: r.内容, accountId: selectVal });
|
||||||
|
reapplyMappings();
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-0.5 bg-indigo-600 text-white rounded hover:bg-indigo-700 shrink-0"
|
||||||
|
>
|
||||||
|
適用
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{override && selectVal === override.accountId && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
removeTransferOverride(r.ID);
|
||||||
|
setOverrideSelects(prev => ({ ...prev, [r.ID]: '' }));
|
||||||
|
reapplyMappings();
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-0.5 text-rose-400 hover:text-rose-300 shrink-0"
|
||||||
|
>
|
||||||
|
解除
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 適用中の振替ルール */}
|
||||||
|
{transferOverrides.length > 0 && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-1">適用中の振替ルール</h3>
|
||||||
|
<p className="text-xs text-slate-500 mb-3">
|
||||||
|
以降のCSVインポートでも「内容」が部分一致する振替取引に自動適用されます。
|
||||||
|
</p>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{transferOverrides.map(o => {
|
||||||
|
const account = accounts.find(a => a.id === o.accountId);
|
||||||
|
const rawDesc = rawRecords.find(r => r.ID === o.mfId)?.内容;
|
||||||
|
const displayDesc = o.description || rawDesc;
|
||||||
|
return (
|
||||||
|
<div key={o.mfId} className="flex items-center gap-2 py-1 border-b border-slate-700/50">
|
||||||
|
<span className="text-xs text-slate-300 flex-1 truncate">
|
||||||
|
{displayDesc
|
||||||
|
? <>{displayDesc} <span className="text-slate-600">({o.mfId.slice(0, 8)}…)</span></>
|
||||||
|
: o.mfId}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-indigo-400 shrink-0">→ {account?.name ?? o.accountId}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => { removeTransferOverride(o.mfId); reapplyMappings(); }}
|
||||||
|
className="text-xs text-rose-500 hover:text-rose-400 shrink-0"
|
||||||
|
>
|
||||||
|
解除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 説明 */}
|
||||||
|
<div className="card border-blue-800 bg-blue-950/30">
|
||||||
|
<h3 className="text-sm font-semibold text-blue-400 mb-2">使い方</h3>
|
||||||
|
<ol className="text-sm text-blue-300 space-y-1 list-decimal list-inside">
|
||||||
|
<li>MoneyForwardの「収入・支出」→「詳細を見る」→「CSVダウンロード」</li>
|
||||||
|
<li>ダウンロードしたCSVファイルをここにドロップ</li>
|
||||||
|
<li>振替取引(口座間移動)は自動でスキップされます</li>
|
||||||
|
<li>マッピング未設定のカテゴリは「設定」画面で勘定科目を割り当てください</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
import { useState, useMemo, useRef, useCallback } from 'react';
|
||||||
|
import { useStore } from '../store';
|
||||||
|
import { getAvailableMonths, filterByMonth } from '../utils/bookkeeping';
|
||||||
|
|
||||||
|
// 列定義
|
||||||
|
const COLUMNS = [
|
||||||
|
{ key: 'date', label: '日付', defaultWidth: 96, align: 'left' },
|
||||||
|
{ key: 'desc', label: '摘要', defaultWidth: 200, align: 'left' },
|
||||||
|
{ key: 'debitAcc', label: '借方勘定', defaultWidth: 130, align: 'left' },
|
||||||
|
{ key: 'debitAmt', label: '借方金額', defaultWidth: 90, align: 'right' },
|
||||||
|
{ key: 'creditAcc', label: '貸方勘定', defaultWidth: 130, align: 'left' },
|
||||||
|
{ key: 'creditAmt', label: '貸方金額', defaultWidth: 90, align: 'right' },
|
||||||
|
{ key: 'mfcat', label: 'MF分類', defaultWidth: 140, align: 'left' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type ColKey = typeof COLUMNS[number]['key'];
|
||||||
|
|
||||||
|
function useColumnResize(defaults: Record<ColKey, number>) {
|
||||||
|
const [widths, setWidths] = useState<Record<ColKey, number>>(defaults);
|
||||||
|
const dragging = useRef<{ key: ColKey; startX: number; startW: number } | null>(null);
|
||||||
|
|
||||||
|
const onMouseDown = useCallback((key: ColKey, e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
dragging.current = { key, startX: e.clientX, startW: widths[key] };
|
||||||
|
|
||||||
|
const onMove = (ev: MouseEvent) => {
|
||||||
|
if (!dragging.current) return;
|
||||||
|
const delta = ev.clientX - dragging.current.startX;
|
||||||
|
const newW = Math.max(50, dragging.current.startW + delta);
|
||||||
|
setWidths(prev => ({ ...prev, [dragging.current!.key]: newW }));
|
||||||
|
};
|
||||||
|
const onUp = () => {
|
||||||
|
dragging.current = null;
|
||||||
|
window.removeEventListener('mousemove', onMove);
|
||||||
|
window.removeEventListener('mouseup', onUp);
|
||||||
|
};
|
||||||
|
window.addEventListener('mousemove', onMove);
|
||||||
|
window.addEventListener('mouseup', onUp);
|
||||||
|
}, [widths]);
|
||||||
|
|
||||||
|
return { widths, onMouseDown };
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_WIDTHS = Object.fromEntries(
|
||||||
|
COLUMNS.map(c => [c.key, c.defaultWidth])
|
||||||
|
) as Record<ColKey, number>;
|
||||||
|
|
||||||
|
export default function JournalPage() {
|
||||||
|
const { transactions, accounts, selectedMonth, setSelectedMonth } = useStore();
|
||||||
|
const accountMap = useMemo(() => new Map(accounts.map(a => [a.id, a])), [accounts]);
|
||||||
|
|
||||||
|
const availableMonths = useMemo(() => getAvailableMonths(transactions), [transactions]);
|
||||||
|
const effectiveMonth = selectedMonth || availableMonths[0] || '';
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const { widths, onMouseDown } = useColumnResize(DEFAULT_WIDTHS);
|
||||||
|
|
||||||
|
const [year, month] = effectiveMonth
|
||||||
|
? [parseInt(effectiveMonth.split('-')[0]), parseInt(effectiveMonth.split('-')[1])]
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
let txs = effectiveMonth ? filterByMonth(transactions, year, month) : transactions;
|
||||||
|
if (search) {
|
||||||
|
const q = search.toLowerCase();
|
||||||
|
txs = txs.filter(
|
||||||
|
t => t.description.toLowerCase().includes(q) ||
|
||||||
|
t.mfCategory?.includes(q) ||
|
||||||
|
t.mfSubCategory?.includes(q) ||
|
||||||
|
t.entries.some(e => accountMap.get(e.accountId)?.name.toLowerCase().includes(q))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [...txs].reverse();
|
||||||
|
}, [transactions, effectiveMonth, year, month, search]);
|
||||||
|
|
||||||
|
if (transactions.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-slate-500">
|
||||||
|
<p className="text-5xl mb-4">📒</p>
|
||||||
|
<p className="text-slate-400">データがありません。CSVをインポートしてください。</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-bold text-slate-100">仕訳帳</h2>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="検索..."
|
||||||
|
value={search}
|
||||||
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
className="border rounded-md px-3 py-1.5 text-sm w-48"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
value={effectiveMonth}
|
||||||
|
onChange={e => setSelectedMonth(e.target.value)}
|
||||||
|
className="border rounded-md px-3 py-1.5 text-sm"
|
||||||
|
>
|
||||||
|
<option value="">全期間</option>
|
||||||
|
{availableMonths.map(m => (
|
||||||
|
<option key={m} value={m}>{m.replace('-', '年')}月</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm text-slate-400">{filtered.length}件</p>
|
||||||
|
|
||||||
|
<div className="card p-0 overflow-auto">
|
||||||
|
<table className="border-collapse" style={{ tableLayout: 'fixed', width: Object.values(widths).reduce((a, b) => a + b, 0) }}>
|
||||||
|
<colgroup>
|
||||||
|
{COLUMNS.map(col => (
|
||||||
|
<col key={col.key} style={{ width: widths[col.key] }} />
|
||||||
|
))}
|
||||||
|
</colgroup>
|
||||||
|
|
||||||
|
<thead className="bg-slate-900 border-b border-slate-700 sticky top-0 z-10">
|
||||||
|
<tr>
|
||||||
|
{COLUMNS.map((col, i) => (
|
||||||
|
<th
|
||||||
|
key={col.key}
|
||||||
|
className="relative px-3 py-2 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider select-none"
|
||||||
|
style={{ width: widths[col.key] }}
|
||||||
|
>
|
||||||
|
<span className={col.align === 'right' ? 'block text-right' : ''}>
|
||||||
|
{col.label}
|
||||||
|
</span>
|
||||||
|
{/* リサイズハンドル(最後の列以外) */}
|
||||||
|
{i < COLUMNS.length - 1 && (
|
||||||
|
<div
|
||||||
|
onMouseDown={e => onMouseDown(col.key, e)}
|
||||||
|
className="absolute top-0 right-0 h-full w-3 flex items-center justify-center cursor-col-resize group"
|
||||||
|
>
|
||||||
|
<div className="w-0.5 h-4 bg-slate-600 group-hover:bg-indigo-400 group-hover:w-1 transition-all rounded-full" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody className="divide-y divide-slate-700/50">
|
||||||
|
{filtered.map(tx => {
|
||||||
|
const debits = tx.entries.filter(e => e.debit > 0);
|
||||||
|
const credits = tx.entries.filter(e => e.credit > 0);
|
||||||
|
const maxRows = Math.max(debits.length, credits.length);
|
||||||
|
|
||||||
|
return Array.from({ length: maxRows }, (_, i) => (
|
||||||
|
<tr key={`${tx.id}-${i}`} className="hover:bg-slate-700/30">
|
||||||
|
{i === 0 && (
|
||||||
|
<>
|
||||||
|
<td className="px-3 py-2 text-sm text-slate-400 align-top overflow-hidden" rowSpan={maxRows}>
|
||||||
|
<span className="block truncate">{tx.date}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm align-top overflow-hidden" rowSpan={maxRows}>
|
||||||
|
<div className="font-medium text-slate-200 text-xs truncate">{tx.description}</div>
|
||||||
|
{tx.mfMemo && <div className="text-slate-500 text-xs truncate">{tx.mfMemo}</div>}
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<td className="px-3 py-2 text-sm text-indigo-400 overflow-hidden">
|
||||||
|
<span className="block truncate">{debits[i] ? accountMap.get(debits[i].accountId)?.name : ''}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm text-right font-medium text-slate-200 overflow-hidden">
|
||||||
|
{debits[i] ? debits[i].debit.toLocaleString() : ''}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm text-rose-400 overflow-hidden">
|
||||||
|
<span className="block truncate">{credits[i] ? accountMap.get(credits[i].accountId)?.name : ''}</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-2 text-sm text-right font-medium text-slate-200 overflow-hidden">
|
||||||
|
{credits[i] ? credits[i].credit.toLocaleString() : ''}
|
||||||
|
</td>
|
||||||
|
{i === 0 && (
|
||||||
|
<td className="px-3 py-2 text-xs text-slate-500 align-top overflow-hidden" rowSpan={maxRows}>
|
||||||
|
<span className="block truncate">
|
||||||
|
{[tx.mfCategory, tx.mfSubCategory].filter(Boolean).join(' / ')}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import type { Page } from '../types';
|
||||||
|
import { useStore } from '../store';
|
||||||
|
|
||||||
|
const NAV_ITEMS: { page: Page; label: string; icon: string }[] = [
|
||||||
|
{ page: 'dashboard', label: 'ダッシュボード', icon: '📊' },
|
||||||
|
{ page: 'journal', label: '仕訳帳', icon: '📒' },
|
||||||
|
{ page: 'ledger', label: '総勘定元帳', icon: '📗' },
|
||||||
|
{ page: 'balancesheet', label: 'バランスシート', icon: '⚖️' },
|
||||||
|
{ page: 'import', label: 'CSVインポート', icon: '📂' },
|
||||||
|
{ page: 'settings', label: '設定・マッピング', icon: '⚙️' },
|
||||||
|
];
|
||||||
|
|
||||||
|
interface LayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Layout({ children }: LayoutProps) {
|
||||||
|
const { currentPage, setCurrentPage, transactions } = useStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen bg-slate-900">
|
||||||
|
{/* サイドバー */}
|
||||||
|
<aside className="w-52 bg-slate-950 flex flex-col shrink-0 border-r border-slate-800">
|
||||||
|
<div className="px-4 py-5 border-b border-indigo-700">
|
||||||
|
<h1 className="text-white font-bold text-base leading-tight">
|
||||||
|
家計簿<br />
|
||||||
|
<span className="text-indigo-300 text-xs font-normal">複式簿記で管理</span>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="flex-1 px-2 py-4 space-y-1">
|
||||||
|
{NAV_ITEMS.map(({ page, label, icon }) => (
|
||||||
|
<button
|
||||||
|
key={page}
|
||||||
|
onClick={() => setCurrentPage(page)}
|
||||||
|
className={`nav-item w-full text-left ${
|
||||||
|
currentPage === page ? 'nav-item-active' : 'nav-item-inactive'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{icon}</span>
|
||||||
|
<span>{label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="px-4 py-3 border-t border-indigo-700">
|
||||||
|
<p className="text-indigo-300 text-xs">
|
||||||
|
仕訳数: {transactions.length.toLocaleString()}件
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* メインコンテンツ */}
|
||||||
|
<main className="flex-1 overflow-y-auto">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useStore } from '../store';
|
||||||
|
import { buildLedger, getAvailableMonths, filterByMonth, formatAmount, calcAccountBalance } from '../utils/bookkeeping';
|
||||||
|
import { ACCOUNT_TYPE_LABELS } from '../utils/accounts';
|
||||||
|
import type { AccountType } from '../types';
|
||||||
|
|
||||||
|
const TYPE_ORDER: AccountType[] = ['asset', 'liability', 'equity', 'income', 'expense'];
|
||||||
|
|
||||||
|
export default function LedgerPage() {
|
||||||
|
const {
|
||||||
|
accounts, transactions, openingBalances,
|
||||||
|
selectedMonth, setSelectedMonth,
|
||||||
|
selectedLedgerAccountId: selectedAccountId,
|
||||||
|
setSelectedLedgerAccountId: setSelectedAccountId,
|
||||||
|
previousPage, goBack,
|
||||||
|
hideInactiveLedgerAccounts, setHideInactiveLedgerAccounts,
|
||||||
|
} = useStore();
|
||||||
|
|
||||||
|
const availableMonths = useMemo(() => getAvailableMonths(transactions), [transactions]);
|
||||||
|
const effectiveMonth = selectedMonth;
|
||||||
|
|
||||||
|
const [year, month] = effectiveMonth
|
||||||
|
? [parseInt(effectiveMonth.split('-')[0]), parseInt(effectiveMonth.split('-')[1])]
|
||||||
|
: [0, 0];
|
||||||
|
|
||||||
|
// 対象取引
|
||||||
|
const targetTxs = useMemo(
|
||||||
|
() => effectiveMonth ? filterByMonth(transactions, year, month) : transactions,
|
||||||
|
[transactions, effectiveMonth, year, month]
|
||||||
|
);
|
||||||
|
|
||||||
|
// 勘定科目グループ
|
||||||
|
const accountGroups = useMemo(() => {
|
||||||
|
return TYPE_ORDER.map(type => ({
|
||||||
|
type,
|
||||||
|
label: ACCOUNT_TYPE_LABELS[type],
|
||||||
|
accounts: accounts
|
||||||
|
.filter(a => a.type === type)
|
||||||
|
.sort((a, b) => a.order - b.order),
|
||||||
|
}));
|
||||||
|
}, [accounts]);
|
||||||
|
|
||||||
|
const selectedAccount = accounts.find(a => a.id === selectedAccountId);
|
||||||
|
|
||||||
|
const ledgerLines = useMemo(() => {
|
||||||
|
if (!selectedAccount) return [];
|
||||||
|
const isBsAccount = selectedAccount.type === 'asset' || selectedAccount.type === 'liability' || selectedAccount.type === 'equity';
|
||||||
|
// B/S科目かつ月フィルター時は「対象月より前の全仕訳による残高」を開始値とする
|
||||||
|
// P/L科目(収益・費用)は月次でリセットされるので期首残高0から始める
|
||||||
|
const startBalance = (isBsAccount && effectiveMonth)
|
||||||
|
? calcAccountBalance(
|
||||||
|
selectedAccount,
|
||||||
|
transactions.filter(tx => tx.date < `${effectiveMonth}-01`),
|
||||||
|
openingBalances,
|
||||||
|
)
|
||||||
|
: (isBsAccount ? (openingBalances.find(b => b.accountId === selectedAccountId)?.amount ?? 0) : 0);
|
||||||
|
return buildLedger(selectedAccount, accounts, targetTxs, startBalance);
|
||||||
|
}, [selectedAccount, accounts, targetTxs, openingBalances, selectedAccountId, effectiveMonth, transactions]);
|
||||||
|
|
||||||
|
// 前月繰越残高(B/S科目かつ月次表示時のみ)
|
||||||
|
const carryOverBalance = useMemo(() => {
|
||||||
|
if (!selectedAccount || !effectiveMonth) return 0;
|
||||||
|
const isBsAccount = selectedAccount.type === 'asset' || selectedAccount.type === 'liability' || selectedAccount.type === 'equity';
|
||||||
|
if (!isBsAccount) return 0;
|
||||||
|
return calcAccountBalance(
|
||||||
|
selectedAccount,
|
||||||
|
transactions.filter(tx => tx.date < `${effectiveMonth}-01`),
|
||||||
|
openingBalances,
|
||||||
|
);
|
||||||
|
}, [selectedAccount, effectiveMonth, transactions, openingBalances]);
|
||||||
|
|
||||||
|
// サマリ(借方合計・貸方合計・残高)
|
||||||
|
const summary = useMemo(() => {
|
||||||
|
const debitTotal = ledgerLines.reduce((s, l) => s + l.debit, 0);
|
||||||
|
const creditTotal = ledgerLines.reduce((s, l) => s + l.credit, 0);
|
||||||
|
const balance = ledgerLines.length > 0 ? ledgerLines[ledgerLines.length - 1].balance : carryOverBalance;
|
||||||
|
return { debitTotal, creditTotal, balance };
|
||||||
|
}, [ledgerLines, carryOverBalance]);
|
||||||
|
|
||||||
|
if (transactions.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-slate-500">
|
||||||
|
<p className="text-5xl mb-4">📗</p>
|
||||||
|
<p className="text-slate-400">データがありません。CSVをインポートしてください。</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-0 h-full overflow-hidden">
|
||||||
|
{/* 左: 勘定科目一覧 */}
|
||||||
|
<div className="w-52 shrink-0 overflow-y-auto border-r border-slate-700 p-4">
|
||||||
|
{previousPage && (
|
||||||
|
<button
|
||||||
|
onClick={goBack}
|
||||||
|
className="flex items-center gap-1 text-xs text-indigo-400 hover:text-indigo-300 mb-3"
|
||||||
|
>
|
||||||
|
← 戻る
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400">勘定科目</h3>
|
||||||
|
<select
|
||||||
|
value={effectiveMonth}
|
||||||
|
onChange={e => setSelectedMonth(e.target.value)}
|
||||||
|
className="border rounded px-1 py-0.5 text-xs"
|
||||||
|
>
|
||||||
|
<option value="">全期間</option>
|
||||||
|
{availableMonths.map(m => (
|
||||||
|
<option key={m} value={m}>{m.replace('-', '年')}月</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-1.5 px-1 mb-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={hideInactiveLedgerAccounts}
|
||||||
|
onChange={e => setHideInactiveLedgerAccounts(e.target.checked)}
|
||||||
|
className="accent-indigo-500 w-3 h-3"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-500">取引なし科目を非表示</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
{accountGroups.map(group => {
|
||||||
|
const visibleAccounts = group.accounts.filter(acc => {
|
||||||
|
const hasActivity = targetTxs.some(tx =>
|
||||||
|
tx.entries.some(e => e.accountId === acc.id)
|
||||||
|
);
|
||||||
|
return !hideInactiveLedgerAccounts || hasActivity;
|
||||||
|
});
|
||||||
|
if (visibleAccounts.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div key={group.type}>
|
||||||
|
<p className="text-xs font-bold text-slate-500 uppercase px-1 mb-1">{group.label}</p>
|
||||||
|
{visibleAccounts.map(acc => {
|
||||||
|
const hasActivity = targetTxs.some(tx =>
|
||||||
|
tx.entries.some(e => e.accountId === acc.id)
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={acc.id}
|
||||||
|
onClick={() => setSelectedAccountId(acc.id)}
|
||||||
|
className={`w-full text-left px-2 py-1.5 rounded text-xs transition-colors ${
|
||||||
|
selectedAccountId === acc.id
|
||||||
|
? 'bg-indigo-700 text-white font-semibold'
|
||||||
|
: hasActivity
|
||||||
|
? 'text-slate-300 hover:bg-slate-700'
|
||||||
|
: 'text-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{acc.name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 右: 元帳明細 */}
|
||||||
|
<div className="flex-1 overflow-hidden flex flex-col p-6 gap-4">
|
||||||
|
{selectedAccount ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-bold text-slate-100">{selectedAccount.name}</h2>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
{ACCOUNT_TYPE_LABELS[selectedAccount.type]} / 正規残高: {selectedAccount.normalBalance === 'debit' ? '借方' : '貸方'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/* サマリ */}
|
||||||
|
<div className="flex gap-4 text-sm">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-slate-500">借方合計</p>
|
||||||
|
<p className="font-semibold text-indigo-400">{summary.debitTotal.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-slate-500">貸方合計</p>
|
||||||
|
<p className="font-semibold text-rose-400">{summary.creditTotal.toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-slate-500">残高</p>
|
||||||
|
<p className={`font-bold ${summary.balance >= 0 ? 'text-slate-100' : 'text-rose-400'}`}>
|
||||||
|
{formatAmount(summary.balance)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card p-0 overflow-auto flex-1">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-slate-900 border-b border-slate-700 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="table-th">日付</th>
|
||||||
|
<th className="table-th">摘要</th>
|
||||||
|
<th className="table-th">相手勘定</th>
|
||||||
|
<th className="table-th text-right">借方</th>
|
||||||
|
<th className="table-th text-right">貸方</th>
|
||||||
|
<th className="table-th text-right">残高</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-slate-700/50">
|
||||||
|
{/* 前月繰越行(月次表示かつ繰越残高あり) */}
|
||||||
|
{effectiveMonth && carryOverBalance !== 0 && (
|
||||||
|
<tr className="bg-slate-800/60 border-b border-slate-600">
|
||||||
|
<td className="table-td text-slate-500 whitespace-nowrap">—</td>
|
||||||
|
<td className="table-td text-slate-500 italic">前月繰越</td>
|
||||||
|
<td className="table-td" />
|
||||||
|
<td className="table-td" />
|
||||||
|
<td className="table-td" />
|
||||||
|
<td className={`table-td text-right font-semibold ${carryOverBalance >= 0 ? 'text-slate-400' : 'text-rose-400'}`}>
|
||||||
|
{carryOverBalance.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
{ledgerLines.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={6} className="text-center py-8 text-slate-500 text-sm">
|
||||||
|
この期間の取引はありません
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
ledgerLines.map((line, i) => (
|
||||||
|
<tr key={i} className="hover:bg-slate-700/30">
|
||||||
|
<td className="table-td text-slate-400 whitespace-nowrap">{line.date}</td>
|
||||||
|
<td className="table-td max-w-xs truncate">{line.description}</td>
|
||||||
|
<td className="table-td text-xs text-slate-500">{line.counterAccountName}</td>
|
||||||
|
<td className="table-td text-right font-medium text-indigo-400">
|
||||||
|
{line.debit > 0 ? line.debit.toLocaleString() : ''}
|
||||||
|
</td>
|
||||||
|
<td className="table-td text-right font-medium text-rose-400">
|
||||||
|
{line.credit > 0 ? line.credit.toLocaleString() : ''}
|
||||||
|
</td>
|
||||||
|
<td className={`table-td text-right font-semibold ${
|
||||||
|
line.balance >= 0 ? 'text-slate-200' : 'text-rose-400'
|
||||||
|
}`}>
|
||||||
|
{line.balance.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-full text-slate-500">
|
||||||
|
<p>左から勘定科目を選択してください</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,483 @@
|
|||||||
|
import { useState, useMemo, useRef } from 'react';
|
||||||
|
import { useStore } from '../store';
|
||||||
|
import type { BackupData } from '../store';
|
||||||
|
import { ACCOUNT_TYPE_LABELS } from '../utils/accounts';
|
||||||
|
|
||||||
|
type Tab = 'category' | 'institution' | 'backup';
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const {
|
||||||
|
accounts, categoryMappings, institutionMappings,
|
||||||
|
upsertCategoryMapping, upsertInstitutionMapping,
|
||||||
|
mergeDefaultMappings, mergeDefaultAccounts,
|
||||||
|
transactions, openingBalances, importedIds, rawRecords, transferOverrides,
|
||||||
|
restoreFromBackup,
|
||||||
|
} = useStore();
|
||||||
|
|
||||||
|
const [tab, setTab] = useState<Tab>('category');
|
||||||
|
const [editCategory, setEditCategory] = useState('');
|
||||||
|
const [editSubCategory, setEditSubCategory] = useState('');
|
||||||
|
const [editAccountId, setEditAccountId] = useState('');
|
||||||
|
const [editInstitution, setEditInstitution] = useState('');
|
||||||
|
const [editInstAccountId, setEditInstAccountId] = useState('');
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [restoreError, setRestoreError] = useState('');
|
||||||
|
const [restoreSuccess, setRestoreSuccess] = useState(false);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// データ内に登場する全カテゴリを抽出(マッピング未設定のものを強調)
|
||||||
|
const allCategories = useMemo(() => {
|
||||||
|
const cats = new Map<string, Set<string>>();
|
||||||
|
for (const tx of transactions) {
|
||||||
|
if (!tx.mfCategory) continue;
|
||||||
|
if (!cats.has(tx.mfCategory)) cats.set(tx.mfCategory, new Set());
|
||||||
|
if (tx.mfSubCategory) cats.get(tx.mfCategory)!.add(tx.mfSubCategory);
|
||||||
|
}
|
||||||
|
return cats;
|
||||||
|
}, [transactions]);
|
||||||
|
|
||||||
|
const allInstitutions = useMemo(() => {
|
||||||
|
return [...new Set(transactions.map(tx => tx.mfInstitution ?? '').filter(Boolean))];
|
||||||
|
}, [transactions]);
|
||||||
|
|
||||||
|
const expenseIncomeAccounts = accounts.filter(
|
||||||
|
a => a.type === 'expense' || a.type === 'income'
|
||||||
|
);
|
||||||
|
const flash = () => {
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExport = () => {
|
||||||
|
const backup: BackupData = {
|
||||||
|
version: 1,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
accounts,
|
||||||
|
transactions,
|
||||||
|
categoryMappings,
|
||||||
|
institutionMappings,
|
||||||
|
openingBalances,
|
||||||
|
importedIds,
|
||||||
|
rawRecords,
|
||||||
|
transferOverrides,
|
||||||
|
};
|
||||||
|
const json = JSON.stringify(backup, null, 2);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
const dateStr = new Date().toISOString().slice(0, 10);
|
||||||
|
a.href = url;
|
||||||
|
a.download = `household-backup-${dateStr}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setRestoreError('');
|
||||||
|
setRestoreSuccess(false);
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (ev) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(ev.target?.result as string) as BackupData;
|
||||||
|
if (data.version !== 1) throw new Error('バージョンが異なります');
|
||||||
|
if (!Array.isArray(data.transactions)) throw new Error('データ形式が不正です');
|
||||||
|
if (!window.confirm(
|
||||||
|
`このバックアップを復元しますか?\n` +
|
||||||
|
`日時: ${new Date(data.exportedAt).toLocaleString()}\n` +
|
||||||
|
`仕訳件数: ${data.transactions.length}件\n\n` +
|
||||||
|
`現在のデータはすべて上書きされます。`
|
||||||
|
)) return;
|
||||||
|
restoreFromBackup(data);
|
||||||
|
setRestoreSuccess(true);
|
||||||
|
setTimeout(() => setRestoreSuccess(false), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
setRestoreError(err instanceof Error ? err.message : '読み込みに失敗しました');
|
||||||
|
} finally {
|
||||||
|
if (fileInputRef.current) fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file, 'utf-8');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveCategory = () => {
|
||||||
|
if (!editCategory || !editAccountId) return;
|
||||||
|
upsertCategoryMapping({
|
||||||
|
mfCategory: editCategory,
|
||||||
|
mfSubCategory: editSubCategory,
|
||||||
|
accountId: editAccountId,
|
||||||
|
});
|
||||||
|
setEditCategory('');
|
||||||
|
setEditSubCategory('');
|
||||||
|
setEditAccountId('');
|
||||||
|
flash();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveInstitution = () => {
|
||||||
|
if (!editInstitution || !editInstAccountId) return;
|
||||||
|
upsertInstitutionMapping({
|
||||||
|
institution: editInstitution,
|
||||||
|
accountId: editInstAccountId,
|
||||||
|
});
|
||||||
|
setEditInstitution('');
|
||||||
|
setEditInstAccountId('');
|
||||||
|
flash();
|
||||||
|
};
|
||||||
|
|
||||||
|
const findCurrentMapping = (cat: string, sub: string) => {
|
||||||
|
const m = categoryMappings.find(
|
||||||
|
m => m.mfCategory === cat && m.mfSubCategory === sub
|
||||||
|
) ?? categoryMappings.find(
|
||||||
|
m => m.mfCategory === cat && m.mfSubCategory === ''
|
||||||
|
);
|
||||||
|
if (!m) return null;
|
||||||
|
if (m.accountId === '_skip_') return 'スキップ(資産間移動)';
|
||||||
|
return accounts.find(a => a.id === m.accountId)?.name ?? m.accountId;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 space-y-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-bold text-slate-100">設定・マッピング</h2>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{saved && (
|
||||||
|
<span className="text-sm text-emerald-400 font-medium">✅ 保存しました</span>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
mergeDefaultAccounts();
|
||||||
|
mergeDefaultMappings();
|
||||||
|
flash();
|
||||||
|
}}
|
||||||
|
className="px-3 py-1.5 text-sm bg-indigo-900/50 text-indigo-300 border border-indigo-700 rounded-md hover:bg-indigo-800"
|
||||||
|
>
|
||||||
|
デフォルトを再適用
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* タブ */}
|
||||||
|
<div className="flex gap-1 border-b border-slate-700">
|
||||||
|
{([['category', 'カテゴリ → 勘定科目'], ['institution', '金融機関 → 資産勘定'], ['backup', 'バックアップ']] as [Tab, string][]).map(
|
||||||
|
([t, label]) => (
|
||||||
|
<button
|
||||||
|
key={t}
|
||||||
|
onClick={() => setTab(t)}
|
||||||
|
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||||
|
tab === t
|
||||||
|
? 'border-indigo-500 text-indigo-400'
|
||||||
|
: 'border-transparent text-slate-500 hover:text-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tab === 'category' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
MoneyForwardの大項目・中項目を費用/収益の勘定科目に対応付けます。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 新規追加フォーム */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-3">マッピングを追加・上書き</h3>
|
||||||
|
<div className="flex gap-2 items-end flex-wrap">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-slate-500 mb-1">大項目</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editCategory}
|
||||||
|
onChange={e => setEditCategory(e.target.value)}
|
||||||
|
list="category-list"
|
||||||
|
className="border rounded px-2 py-1.5 text-sm w-36"
|
||||||
|
placeholder="食費"
|
||||||
|
/>
|
||||||
|
<datalist id="category-list">
|
||||||
|
{[...allCategories.keys()].map(c => (
|
||||||
|
<option key={c} value={c} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-slate-500 mb-1">中項目(空=大項目全体)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editSubCategory}
|
||||||
|
onChange={e => setEditSubCategory(e.target.value)}
|
||||||
|
list="subcategory-list"
|
||||||
|
className="border rounded px-2 py-1.5 text-sm w-36"
|
||||||
|
placeholder="外食(任意)"
|
||||||
|
/>
|
||||||
|
<datalist id="subcategory-list">
|
||||||
|
{editCategory && allCategories.get(editCategory)
|
||||||
|
? [...allCategories.get(editCategory)!].map(s => (
|
||||||
|
<option key={s} value={s} />
|
||||||
|
))
|
||||||
|
: null}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-slate-500 mb-1">勘定科目</label>
|
||||||
|
<select
|
||||||
|
value={editAccountId}
|
||||||
|
onChange={e => setEditAccountId(e.target.value)}
|
||||||
|
className="border rounded px-2 py-1.5 text-sm w-44"
|
||||||
|
>
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
<option value="_skip_">⏭ スキップ(資産間移動)</option>
|
||||||
|
{(['income', 'expense'] as const).map(type => (
|
||||||
|
<optgroup key={type} label={ACCOUNT_TYPE_LABELS[type]}>
|
||||||
|
{expenseIncomeAccounts
|
||||||
|
.filter(a => a.type === type)
|
||||||
|
.map(a => (
|
||||||
|
<option key={a.id} value={a.id}>{a.name}</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveCategory}
|
||||||
|
disabled={!editCategory || !editAccountId}
|
||||||
|
className="px-4 py-1.5 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700 disabled:opacity-40"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* データ内カテゴリ一覧 */}
|
||||||
|
{allCategories.size > 0 && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-3">
|
||||||
|
データ内のカテゴリ(クリックで編集欄へ)
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[...allCategories.entries()].map(([cat, subs]) => {
|
||||||
|
const subList = ['', ...[...subs]];
|
||||||
|
return subList.map(sub => {
|
||||||
|
const mapped = findCurrentMapping(cat, sub);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={`${cat}-${sub}`}
|
||||||
|
onClick={() => { setEditCategory(cat); setEditSubCategory(sub); }}
|
||||||
|
className="flex items-center justify-between py-1 px-2 rounded hover:bg-slate-700 cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm text-slate-300">{cat}</span>
|
||||||
|
{sub && (
|
||||||
|
<>
|
||||||
|
<span className="text-slate-600">/</span>
|
||||||
|
<span className="text-sm text-slate-400">{sub}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!sub && <span className="text-xs text-slate-600">(大項目全体)</span>}
|
||||||
|
</div>
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||||
|
mapped ? 'bg-emerald-900/50 text-emerald-400' : 'bg-amber-900/50 text-amber-400'
|
||||||
|
}`}>
|
||||||
|
{mapped ?? '未設定'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 現在のマッピング一覧 */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-3">現在のマッピング(全件)</h3>
|
||||||
|
<div className="max-h-64 overflow-y-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead className="sticky top-0 bg-slate-800">
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="table-th">大項目</th>
|
||||||
|
<th className="table-th">中項目</th>
|
||||||
|
<th className="table-th">勘定科目</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{categoryMappings.map((m, i) => (
|
||||||
|
<tr
|
||||||
|
key={i}
|
||||||
|
className="border-b border-slate-700/50 hover:bg-slate-700 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setEditCategory(m.mfCategory);
|
||||||
|
setEditSubCategory(m.mfSubCategory);
|
||||||
|
setEditAccountId(m.accountId);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<td className="table-td">{m.mfCategory}</td>
|
||||||
|
<td className="table-td text-slate-500">{m.mfSubCategory || '(全体)'}</td>
|
||||||
|
<td className={`table-td ${m.accountId === '_skip_' ? 'text-slate-500 italic' : 'text-indigo-400'}`}>
|
||||||
|
{m.accountId === '_skip_'
|
||||||
|
? 'スキップ(資産間移動)'
|
||||||
|
: accounts.find(a => a.id === m.accountId)?.name ?? m.accountId}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'backup' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
全データをJSONファイルに保存・復元します。ブラウザのデータが消えた場合の備えとして定期的にエクスポートしてください。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* エクスポート */}
|
||||||
|
<div className="card space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-300">エクスポート(バックアップ作成)</h3>
|
||||||
|
<div className="text-sm text-slate-400 space-y-1">
|
||||||
|
<div>仕訳件数: <span className="text-slate-200">{transactions.length.toLocaleString()} 件</span></div>
|
||||||
|
<div>勘定科目: <span className="text-slate-200">{accounts.length} 件</span></div>
|
||||||
|
<div>カテゴリマッピング: <span className="text-slate-200">{categoryMappings.length} 件</span></div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleExport}
|
||||||
|
className="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700"
|
||||||
|
>
|
||||||
|
JSONをダウンロード
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* インポート(復元) */}
|
||||||
|
<div className="card space-y-3">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-300">インポート(バックアップから復元)</h3>
|
||||||
|
<p className="text-xs text-amber-400">
|
||||||
|
現在のデータはすべて上書きされます。復元前に必ずエクスポートで保存してください。
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={handleImport}
|
||||||
|
className="block text-sm text-slate-400 file:mr-3 file:py-1.5 file:px-3 file:rounded file:border-0 file:text-sm file:bg-slate-700 file:text-slate-200 hover:file:bg-slate-600 cursor-pointer"
|
||||||
|
/>
|
||||||
|
{restoreError && (
|
||||||
|
<p className="text-sm text-red-400">エラー: {restoreError}</p>
|
||||||
|
)}
|
||||||
|
{restoreSuccess && (
|
||||||
|
<p className="text-sm text-emerald-400">復元しました</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tab === 'institution' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
MoneyForwardの保有金融機関を資産勘定科目に対応付けます。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* 新規追加フォーム */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-3">マッピングを追加・上書き</h3>
|
||||||
|
<div className="flex gap-2 items-end">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-slate-500 mb-1">保有金融機関</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editInstitution}
|
||||||
|
onChange={e => setEditInstitution(e.target.value)}
|
||||||
|
list="institution-list"
|
||||||
|
className="border rounded px-2 py-1.5 text-sm w-52"
|
||||||
|
placeholder="SBI銀行"
|
||||||
|
/>
|
||||||
|
<datalist id="institution-list">
|
||||||
|
{allInstitutions.map(inst => (
|
||||||
|
<option key={inst} value={inst} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs text-slate-500 mb-1">資産勘定科目</label>
|
||||||
|
<select
|
||||||
|
value={editInstAccountId}
|
||||||
|
onChange={e => setEditInstAccountId(e.target.value)}
|
||||||
|
className="border rounded px-2 py-1.5 text-sm w-44"
|
||||||
|
>
|
||||||
|
<option value="">選択してください</option>
|
||||||
|
{(['asset', 'liability'] as const).map(type => (
|
||||||
|
<optgroup key={type} label={ACCOUNT_TYPE_LABELS[type]}>
|
||||||
|
{accounts.filter(a => a.type === type).map(a => (
|
||||||
|
<option key={a.id} value={a.id}>{a.name}</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={handleSaveInstitution}
|
||||||
|
disabled={!editInstitution || !editInstAccountId}
|
||||||
|
className="px-4 py-1.5 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700 disabled:opacity-40"
|
||||||
|
>
|
||||||
|
保存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* データ内金融機関 */}
|
||||||
|
{allInstitutions.length > 0 && (
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-3">データ内の金融機関</h3>
|
||||||
|
<div className="space-y-1">
|
||||||
|
{allInstitutions.map(inst => {
|
||||||
|
const m = institutionMappings.find(im => im.institution === inst);
|
||||||
|
const accName = m ? accounts.find(a => a.id === m.accountId)?.name : null;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={inst}
|
||||||
|
onClick={() => setEditInstitution(inst)}
|
||||||
|
className="flex items-center justify-between py-1 px-2 rounded hover:bg-slate-700 cursor-pointer"
|
||||||
|
>
|
||||||
|
<span className="text-sm text-slate-300">{inst}</span>
|
||||||
|
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||||
|
accName ? 'bg-emerald-900/50 text-emerald-400' : 'bg-amber-900/50 text-amber-400'
|
||||||
|
}`}>
|
||||||
|
{accName ?? '未設定'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 現在のマッピング */}
|
||||||
|
<div className="card">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-400 mb-3">現在のマッピング</h3>
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-slate-700">
|
||||||
|
<th className="table-th">保有金融機関</th>
|
||||||
|
<th className="table-th">勘定科目</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{institutionMappings.map((m, i) => (
|
||||||
|
<tr key={i} className="border-b border-slate-700/50 hover:bg-slate-700">
|
||||||
|
<td className="table-td">{m.institution}</td>
|
||||||
|
<td className="table-td text-indigo-400">
|
||||||
|
{accounts.find(a => a.id === m.accountId)?.name ?? m.accountId}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Noto Sans JP', sans-serif;
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark form elements */
|
||||||
|
select,
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="search"] {
|
||||||
|
background-color: #1e293b !important;
|
||||||
|
border-color: #475569 !important;
|
||||||
|
color: #f1f5f9 !important;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option {
|
||||||
|
background-color: #1e293b;
|
||||||
|
color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder {
|
||||||
|
color: #64748b !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.nav-item {
|
||||||
|
@apply flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors;
|
||||||
|
}
|
||||||
|
.nav-item-active {
|
||||||
|
@apply bg-indigo-700 text-white;
|
||||||
|
}
|
||||||
|
.nav-item-inactive {
|
||||||
|
@apply text-indigo-200 hover:bg-indigo-700 hover:text-white;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
@apply bg-slate-800 rounded-lg shadow-sm border border-slate-700 p-4;
|
||||||
|
}
|
||||||
|
.amount-positive {
|
||||||
|
@apply text-emerald-400 font-medium;
|
||||||
|
}
|
||||||
|
.amount-negative {
|
||||||
|
@apply text-rose-400 font-medium;
|
||||||
|
}
|
||||||
|
.table-th {
|
||||||
|
@apply px-3 py-2 text-left text-xs font-semibold text-slate-400 uppercase tracking-wider;
|
||||||
|
}
|
||||||
|
.table-td {
|
||||||
|
@apply px-3 py-2 text-sm text-slate-300;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { StateStorage } from 'zustand/middleware';
|
||||||
|
|
||||||
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
|
||||||
|
async function saveToFile(key: string, value: string) {
|
||||||
|
await fetch('/api/data', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ key, value: JSON.parse(value) }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileStorage: StateStorage = {
|
||||||
|
async getItem(name) {
|
||||||
|
const res = await fetch('/api/data');
|
||||||
|
const data = await res.json() as Record<string, unknown>;
|
||||||
|
const val = data[name];
|
||||||
|
return val !== undefined ? JSON.stringify(val) : null;
|
||||||
|
},
|
||||||
|
|
||||||
|
setItem(name, value) {
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => saveToFile(name, value), 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
async removeItem(name) {
|
||||||
|
await fetch(`/api/data?key=${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
|
import { fileStorage } from '../lib/fileStorage';
|
||||||
|
import type {
|
||||||
|
Account, Transaction, CategoryMapping, InstitutionMapping,
|
||||||
|
OpeningBalance, Page, MFRecord, TransferOverride,
|
||||||
|
} from '../types';
|
||||||
|
import {
|
||||||
|
DEFAULT_ACCOUNTS,
|
||||||
|
DEFAULT_CATEGORY_MAPPINGS,
|
||||||
|
DEFAULT_INSTITUTION_MAPPINGS,
|
||||||
|
} from '../utils/accounts';
|
||||||
|
import { convertToTransactions } from '../utils/csvParser';
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
// ナビゲーション
|
||||||
|
currentPage: Page;
|
||||||
|
setCurrentPage: (page: Page) => void;
|
||||||
|
previousPage: Page | null;
|
||||||
|
navigateTo: (page: Page) => void;
|
||||||
|
goBack: () => void;
|
||||||
|
|
||||||
|
// 共有選択月(全ページで同期)
|
||||||
|
selectedMonth: string;
|
||||||
|
setSelectedMonth: (month: string) => void;
|
||||||
|
|
||||||
|
// 総勘定元帳の選択勘定科目(ページ間遷移で引き継ぐ)
|
||||||
|
selectedLedgerAccountId: string;
|
||||||
|
setSelectedLedgerAccountId: (id: string) => void;
|
||||||
|
|
||||||
|
// バランスシートの選択タブ(ページ間遷移で引き継ぐ)
|
||||||
|
balancesheetTab: 'bs' | 'pl';
|
||||||
|
setBalancesheetTab: (tab: 'bs' | 'pl') => void;
|
||||||
|
|
||||||
|
// 勘定科目マスタ
|
||||||
|
accounts: Account[];
|
||||||
|
setAccounts: (accounts: Account[]) => void;
|
||||||
|
|
||||||
|
// 仕訳データ
|
||||||
|
transactions: Transaction[];
|
||||||
|
addTransactions: (txs: Transaction[]) => void;
|
||||||
|
upsertTransactions: (txs: Transaction[]) => void;
|
||||||
|
removeTransactionsByMonth: (yearMonth: string) => void;
|
||||||
|
clearTransactions: () => void;
|
||||||
|
|
||||||
|
// カテゴリマッピング
|
||||||
|
categoryMappings: CategoryMapping[];
|
||||||
|
setCategoryMappings: (mappings: CategoryMapping[]) => void;
|
||||||
|
upsertCategoryMapping: (mapping: CategoryMapping) => void;
|
||||||
|
|
||||||
|
// 金融機関マッピング
|
||||||
|
institutionMappings: InstitutionMapping[];
|
||||||
|
setInstitutionMappings: (mappings: InstitutionMapping[]) => void;
|
||||||
|
upsertInstitutionMapping: (mapping: InstitutionMapping) => void;
|
||||||
|
|
||||||
|
// 期首残高
|
||||||
|
openingBalances: OpeningBalance[];
|
||||||
|
upsertOpeningBalance: (balance: OpeningBalance) => void;
|
||||||
|
|
||||||
|
// 振替スキップ除外(特定の振替取引を収入/費用として処理)
|
||||||
|
transferOverrides: TransferOverride[];
|
||||||
|
upsertTransferOverride: (override: TransferOverride) => void;
|
||||||
|
removeTransferOverride: (mfId: string) => void;
|
||||||
|
|
||||||
|
// インポート済みMF-IDセット(重複防止)
|
||||||
|
importedIds: string[];
|
||||||
|
addImportedIds: (ids: string[]) => void;
|
||||||
|
|
||||||
|
// 生のMFRecord(マッピング再適用のために保持)
|
||||||
|
rawRecords: MFRecord[];
|
||||||
|
addRawRecords: (records: MFRecord[]) => void;
|
||||||
|
upsertRawRecords: (records: MFRecord[]) => void;
|
||||||
|
|
||||||
|
// マッピングを再適用(生データから仕訳を再生成)
|
||||||
|
reapplyMappings: () => { added: number; unmapped: MFRecord[] };
|
||||||
|
|
||||||
|
// デフォルトを現在のマッピングにマージ(未設定のものだけ追加)
|
||||||
|
mergeDefaultMappings: () => void;
|
||||||
|
|
||||||
|
// 勘定科目マスタをデフォルトに新規分を追加
|
||||||
|
mergeDefaultAccounts: () => void;
|
||||||
|
|
||||||
|
// 総勘定元帳: 取引なし科目を非表示にする
|
||||||
|
hideInactiveLedgerAccounts: boolean;
|
||||||
|
setHideInactiveLedgerAccounts: (v: boolean) => void;
|
||||||
|
|
||||||
|
// バックアップデータを復元
|
||||||
|
restoreFromBackup: (data: BackupData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BackupData {
|
||||||
|
version: number;
|
||||||
|
exportedAt: string;
|
||||||
|
accounts: Account[];
|
||||||
|
transactions: Transaction[];
|
||||||
|
categoryMappings: CategoryMapping[];
|
||||||
|
institutionMappings: InstitutionMapping[];
|
||||||
|
openingBalances: OpeningBalance[];
|
||||||
|
importedIds: string[];
|
||||||
|
rawRecords: MFRecord[];
|
||||||
|
transferOverrides?: TransferOverride[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStore = create<AppState>()(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
previousPage: null,
|
||||||
|
setCurrentPage: (page) => set({ currentPage: page, previousPage: null }),
|
||||||
|
navigateTo: (page) => set((state) => ({ previousPage: state.currentPage, currentPage: page })),
|
||||||
|
goBack: () => set((state) => state.previousPage ? { currentPage: state.previousPage, previousPage: null } : {}),
|
||||||
|
|
||||||
|
selectedMonth: '',
|
||||||
|
setSelectedMonth: (month) => set({ selectedMonth: month }),
|
||||||
|
|
||||||
|
selectedLedgerAccountId: '',
|
||||||
|
setSelectedLedgerAccountId: (id) => set({ selectedLedgerAccountId: id }),
|
||||||
|
|
||||||
|
balancesheetTab: 'bs',
|
||||||
|
setBalancesheetTab: (tab) => set({ balancesheetTab: tab }),
|
||||||
|
|
||||||
|
hideInactiveLedgerAccounts: false,
|
||||||
|
setHideInactiveLedgerAccounts: (v) => set({ hideInactiveLedgerAccounts: v }),
|
||||||
|
|
||||||
|
accounts: DEFAULT_ACCOUNTS,
|
||||||
|
setAccounts: (accounts) => set({ accounts }),
|
||||||
|
|
||||||
|
transactions: [],
|
||||||
|
addTransactions: (txs) =>
|
||||||
|
set((state) => ({
|
||||||
|
transactions: [...state.transactions, ...txs].sort((a, b) =>
|
||||||
|
a.date.localeCompare(b.date)
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
upsertTransactions: (txs) =>
|
||||||
|
set((state) => {
|
||||||
|
const upsertMfIds = new Set(txs.map(t => t.mfId).filter(Boolean));
|
||||||
|
const upsertFallbackIds = new Set(txs.filter(t => !t.mfId).map(t => t.id));
|
||||||
|
const kept = state.transactions.filter(t =>
|
||||||
|
t.mfId ? !upsertMfIds.has(t.mfId) : !upsertFallbackIds.has(t.id)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
transactions: [...kept, ...txs].sort((a, b) => a.date.localeCompare(b.date)),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
removeTransactionsByMonth: (yearMonth) =>
|
||||||
|
set((state) => ({
|
||||||
|
transactions: state.transactions.filter(
|
||||||
|
(tx) => !tx.date.startsWith(yearMonth)
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
clearTransactions: () => set({ transactions: [], importedIds: [], rawRecords: [] }),
|
||||||
|
|
||||||
|
categoryMappings: DEFAULT_CATEGORY_MAPPINGS,
|
||||||
|
setCategoryMappings: (mappings) => set({ categoryMappings: mappings }),
|
||||||
|
upsertCategoryMapping: (mapping) =>
|
||||||
|
set((state) => {
|
||||||
|
const existing = state.categoryMappings.findIndex(
|
||||||
|
(m) =>
|
||||||
|
m.mfCategory === mapping.mfCategory &&
|
||||||
|
m.mfSubCategory === mapping.mfSubCategory
|
||||||
|
);
|
||||||
|
if (existing >= 0) {
|
||||||
|
const next = [...state.categoryMappings];
|
||||||
|
next[existing] = mapping;
|
||||||
|
return { categoryMappings: next };
|
||||||
|
}
|
||||||
|
return { categoryMappings: [...state.categoryMappings, mapping] };
|
||||||
|
}),
|
||||||
|
|
||||||
|
institutionMappings: DEFAULT_INSTITUTION_MAPPINGS,
|
||||||
|
setInstitutionMappings: (mappings) => set({ institutionMappings: mappings }),
|
||||||
|
upsertInstitutionMapping: (mapping) =>
|
||||||
|
set((state) => {
|
||||||
|
const existing = state.institutionMappings.findIndex(
|
||||||
|
(m) => m.institution === mapping.institution
|
||||||
|
);
|
||||||
|
if (existing >= 0) {
|
||||||
|
const next = [...state.institutionMappings];
|
||||||
|
next[existing] = mapping;
|
||||||
|
return { institutionMappings: next };
|
||||||
|
}
|
||||||
|
return { institutionMappings: [...state.institutionMappings, mapping] };
|
||||||
|
}),
|
||||||
|
|
||||||
|
openingBalances: [],
|
||||||
|
upsertOpeningBalance: (balance) =>
|
||||||
|
set((state) => {
|
||||||
|
const existing = state.openingBalances.findIndex(
|
||||||
|
(b) => b.accountId === balance.accountId
|
||||||
|
);
|
||||||
|
if (existing >= 0) {
|
||||||
|
const next = [...state.openingBalances];
|
||||||
|
next[existing] = balance;
|
||||||
|
return { openingBalances: next };
|
||||||
|
}
|
||||||
|
return { openingBalances: [...state.openingBalances, balance] };
|
||||||
|
}),
|
||||||
|
|
||||||
|
transferOverrides: [],
|
||||||
|
upsertTransferOverride: (override) =>
|
||||||
|
set((state) => {
|
||||||
|
const existing = state.transferOverrides.findIndex(o => o.mfId === override.mfId);
|
||||||
|
if (existing >= 0) {
|
||||||
|
const next = [...state.transferOverrides];
|
||||||
|
next[existing] = override;
|
||||||
|
return { transferOverrides: next };
|
||||||
|
}
|
||||||
|
return { transferOverrides: [...state.transferOverrides, override] };
|
||||||
|
}),
|
||||||
|
removeTransferOverride: (mfId) =>
|
||||||
|
set((state) => ({
|
||||||
|
transferOverrides: state.transferOverrides.filter(o => o.mfId !== mfId),
|
||||||
|
})),
|
||||||
|
|
||||||
|
importedIds: [],
|
||||||
|
addImportedIds: (ids) =>
|
||||||
|
set((state) => ({
|
||||||
|
importedIds: [...new Set([...state.importedIds, ...ids])],
|
||||||
|
})),
|
||||||
|
|
||||||
|
rawRecords: [],
|
||||||
|
addRawRecords: (records) =>
|
||||||
|
set((state) => ({
|
||||||
|
rawRecords: [...state.rawRecords, ...records],
|
||||||
|
})),
|
||||||
|
upsertRawRecords: (records) =>
|
||||||
|
set((state) => {
|
||||||
|
const upsertIds = new Set(records.map(r => r.ID).filter(Boolean));
|
||||||
|
const kept = state.rawRecords.filter(r => !r.ID || !upsertIds.has(r.ID));
|
||||||
|
return { rawRecords: [...kept, ...records] };
|
||||||
|
}),
|
||||||
|
|
||||||
|
reapplyMappings: () => {
|
||||||
|
const state = get();
|
||||||
|
const { transactions: newTxs, unmapped } = convertToTransactions(
|
||||||
|
state.rawRecords,
|
||||||
|
state.categoryMappings,
|
||||||
|
state.institutionMappings,
|
||||||
|
new Set<string>(),
|
||||||
|
state.accounts,
|
||||||
|
state.transferOverrides,
|
||||||
|
);
|
||||||
|
const newIds = newTxs.map(t => t.mfId ?? t.id).filter(Boolean);
|
||||||
|
set({
|
||||||
|
transactions: newTxs.sort((a, b) => a.date.localeCompare(b.date)),
|
||||||
|
importedIds: newIds,
|
||||||
|
});
|
||||||
|
return { added: newTxs.length, unmapped };
|
||||||
|
},
|
||||||
|
|
||||||
|
mergeDefaultMappings: () =>
|
||||||
|
set((state) => {
|
||||||
|
// カテゴリ: 既存にないものだけ追加
|
||||||
|
const catMappings = [...state.categoryMappings];
|
||||||
|
for (const dm of DEFAULT_CATEGORY_MAPPINGS) {
|
||||||
|
const exists = catMappings.some(
|
||||||
|
m => m.mfCategory === dm.mfCategory && m.mfSubCategory === dm.mfSubCategory
|
||||||
|
);
|
||||||
|
if (!exists) catMappings.push(dm);
|
||||||
|
}
|
||||||
|
// 金融機関: 既存にないものだけ追加
|
||||||
|
const instMappings = [...state.institutionMappings];
|
||||||
|
for (const dm of DEFAULT_INSTITUTION_MAPPINGS) {
|
||||||
|
const exists = instMappings.some(m => m.institution === dm.institution);
|
||||||
|
if (!exists) instMappings.push(dm);
|
||||||
|
}
|
||||||
|
return { categoryMappings: catMappings, institutionMappings: instMappings };
|
||||||
|
}),
|
||||||
|
|
||||||
|
mergeDefaultAccounts: () =>
|
||||||
|
set((state) => {
|
||||||
|
const accounts = [...state.accounts];
|
||||||
|
for (const da of DEFAULT_ACCOUNTS) {
|
||||||
|
const exists = accounts.some(a => a.id === da.id);
|
||||||
|
if (!exists) accounts.push(da);
|
||||||
|
}
|
||||||
|
return { accounts: accounts.sort((a, b) => a.order - b.order) };
|
||||||
|
}),
|
||||||
|
|
||||||
|
restoreFromBackup: (data) =>
|
||||||
|
set({
|
||||||
|
accounts: data.accounts,
|
||||||
|
transactions: data.transactions,
|
||||||
|
categoryMappings: data.categoryMappings,
|
||||||
|
institutionMappings: data.institutionMappings,
|
||||||
|
openingBalances: data.openingBalances,
|
||||||
|
importedIds: data.importedIds,
|
||||||
|
rawRecords: data.rawRecords ?? [],
|
||||||
|
transferOverrides: data.transferOverrides ?? [],
|
||||||
|
}),
|
||||||
|
|
||||||
|
// 現在のstateを参照するためのゲッター
|
||||||
|
_get: get,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'household-bookkeeping-store',
|
||||||
|
storage: createJSONStorage(() => fileStorage),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// 勘定科目の種別
|
||||||
|
export type AccountType = 'asset' | 'liability' | 'equity' | 'income' | 'expense';
|
||||||
|
|
||||||
|
// 勘定科目
|
||||||
|
export interface Account {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: AccountType;
|
||||||
|
// 借方残高が正 (資産・費用) か 貸方残高が正 (負債・純資産・収益) か
|
||||||
|
normalBalance: 'debit' | 'credit';
|
||||||
|
order: number; // 表示順
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仕訳明細
|
||||||
|
export interface JournalEntry {
|
||||||
|
accountId: string;
|
||||||
|
debit: number;
|
||||||
|
credit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 仕訳(取引)
|
||||||
|
export interface Transaction {
|
||||||
|
id: string;
|
||||||
|
date: string; // YYYY-MM-DD
|
||||||
|
description: string;
|
||||||
|
entries: JournalEntry[];
|
||||||
|
// MoneyForward 元データ
|
||||||
|
mfInstitution?: string; // 保有金融機関
|
||||||
|
mfCategory?: string; // 大項目
|
||||||
|
mfSubCategory?: string; // 中項目
|
||||||
|
mfMemo?: string; // メモ
|
||||||
|
mfId?: string; // ID
|
||||||
|
isTransfer?: boolean; // 振替フラグ
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoneyForward CSV 1行
|
||||||
|
export interface MFRecord {
|
||||||
|
計算対象: string;
|
||||||
|
日付: string;
|
||||||
|
内容: string;
|
||||||
|
金額: number;
|
||||||
|
保有金融機関: string;
|
||||||
|
大項目: string;
|
||||||
|
中項目: string;
|
||||||
|
メモ: string;
|
||||||
|
振替: string;
|
||||||
|
ID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// カテゴリマッピング(MoneyForwardカテゴリ → 勘定科目ID)
|
||||||
|
export interface CategoryMapping {
|
||||||
|
mfCategory: string; // 大項目
|
||||||
|
mfSubCategory: string; // 中項目(空の場合は大項目全体に適用)
|
||||||
|
accountId: string; // 費用/収益 勘定科目ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 金融機関マッピング(保有金融機関 → 資産勘定科目ID)
|
||||||
|
export interface InstitutionMapping {
|
||||||
|
institution: string;
|
||||||
|
accountId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 期首残高
|
||||||
|
export interface OpeningBalance {
|
||||||
|
accountId: string;
|
||||||
|
amount: number; // 正規残高の方向で
|
||||||
|
}
|
||||||
|
|
||||||
|
// 振替スキップ除外(特定の振替取引を収入/費用として扱う)
|
||||||
|
export interface TransferOverride {
|
||||||
|
mfId: string; // 最初に設定した MF ID(識別キー)
|
||||||
|
description: string; // 内容テキスト(部分一致で以降のCSVにも適用)
|
||||||
|
accountId: string; // 振り替える収益/費用 勘定科目ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ページ種別
|
||||||
|
export type Page = 'dashboard' | 'import' | 'journal' | 'ledger' | 'balancesheet' | 'settings';
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import type { Account, CategoryMapping, InstitutionMapping } from '../types';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// デフォルト勘定科目マスタ
|
||||||
|
// ============================================================
|
||||||
|
export const DEFAULT_ACCOUNTS: Account[] = [
|
||||||
|
// 資産
|
||||||
|
{ id: 'cash', name: '現金・財布', type: 'asset', normalBalance: 'debit', order: 10 },
|
||||||
|
{ id: 'bank_sbi', name: '住信SBIネット銀行', type: 'asset', normalBalance: 'debit', order: 11 },
|
||||||
|
{ id: 'bank_smbc_olive', name: '三井住友銀行(Olive)', type: 'asset', normalBalance: 'debit', order: 12 },
|
||||||
|
{ id: 'bank_mufg', name: '三菱UFJ銀行', type: 'asset', normalBalance: 'debit', order: 13 },
|
||||||
|
{ id: 'bank_rakuten', name: '楽天銀行', type: 'asset', normalBalance: 'debit', order: 14 },
|
||||||
|
{ id: 'bank_other', name: 'その他銀行', type: 'asset', normalBalance: 'debit', order: 15 },
|
||||||
|
{ id: 'suica', name: 'Suica・ICカード', type: 'asset', normalBalance: 'debit', order: 16 },
|
||||||
|
{ id: 'credit_asset', name: 'クレジットカード資産', type: 'asset', normalBalance: 'debit', order: 17 },
|
||||||
|
{ id: 'investment', name: '投資・証券', type: 'asset', normalBalance: 'debit', order: 18 },
|
||||||
|
{ id: 'point', name: 'ポイント・マイル', type: 'asset', normalBalance: 'debit', order: 19 },
|
||||||
|
{ id: 'other_asset', name: 'その他資産', type: 'asset', normalBalance: 'debit', order: 20 },
|
||||||
|
|
||||||
|
// 負債
|
||||||
|
{ id: 'credit_smbc', name: '三井住友カード', type: 'liability', normalBalance: 'credit', order: 30 },
|
||||||
|
{ id: 'credit_amazon', name: 'Amazonカード', type: 'liability', normalBalance: 'credit', order: 31 },
|
||||||
|
{ id: 'credit_other', name: 'その他クレジット', type: 'liability', normalBalance: 'credit', order: 32 },
|
||||||
|
{ id: 'loan', name: 'ローン', type: 'liability', normalBalance: 'credit', order: 33 },
|
||||||
|
{ id: 'other_liability', name: 'その他負債', type: 'liability', normalBalance: 'credit', order: 34 },
|
||||||
|
|
||||||
|
// 純資産
|
||||||
|
{ id: 'opening_equity', name: '期首純資産', type: 'equity', normalBalance: 'credit', order: 50 },
|
||||||
|
|
||||||
|
// 収益
|
||||||
|
{ id: 'income_salary', name: '給与収入', type: 'income', normalBalance: 'credit', order: 60 },
|
||||||
|
{ id: 'income_bonus', name: '賞与収入', type: 'income', normalBalance: 'credit', order: 61 },
|
||||||
|
{ id: 'income_side', name: '副業・フリー収入', type: 'income', normalBalance: 'credit', order: 62 },
|
||||||
|
{ id: 'income_investment', name: '投資収益', type: 'income', normalBalance: 'credit', order: 63 },
|
||||||
|
{ id: 'income_point', name: 'ポイント還元・キャッシュバック', type: 'income', normalBalance: 'credit', order: 64 },
|
||||||
|
{ id: 'income_other', name: 'その他収入', type: 'income', normalBalance: 'credit', order: 65 },
|
||||||
|
|
||||||
|
// 費用
|
||||||
|
{ id: 'exp_food_grocery', name: '食費(食料品)', type: 'expense', normalBalance: 'debit', order: 70 },
|
||||||
|
{ id: 'exp_food_dining', name: '食費(外食・カフェ)', type: 'expense', normalBalance: 'debit', order: 71 },
|
||||||
|
{ id: 'exp_daily', name: '日用品', type: 'expense', normalBalance: 'debit', order: 72 },
|
||||||
|
{ id: 'exp_transport', name: '交通費', type: 'expense', normalBalance: 'debit', order: 73 },
|
||||||
|
{ id: 'exp_car', name: '自動車関連', type: 'expense', normalBalance: 'debit', order: 74 },
|
||||||
|
{ id: 'exp_comms', name: '通信費', type: 'expense', normalBalance: 'debit', order: 75 },
|
||||||
|
{ id: 'exp_electric', name: '電気代', type: 'expense', normalBalance: 'debit', order: 76 },
|
||||||
|
{ id: 'exp_gas', name: 'ガス代', type: 'expense', normalBalance: 'debit', order: 77 },
|
||||||
|
{ id: 'exp_water', name: '水道代', type: 'expense', normalBalance: 'debit', order: 78 },
|
||||||
|
{ id: 'exp_housing', name: '住居費', type: 'expense', normalBalance: 'debit', order: 79 },
|
||||||
|
{ id: 'exp_medical', name: '医療費', type: 'expense', normalBalance: 'debit', order: 80 },
|
||||||
|
{ id: 'exp_entertainment', name: '娯楽費', type: 'expense', normalBalance: 'debit', order: 81 },
|
||||||
|
{ id: 'exp_subscription', name: 'サブスク・会費', type: 'expense', normalBalance: 'debit', order: 82 },
|
||||||
|
{ id: 'exp_book', name: '書籍・教育', type: 'expense', normalBalance: 'debit', order: 83 },
|
||||||
|
{ id: 'exp_clothing', name: '衣服・美容', type: 'expense', normalBalance: 'debit', order: 84 },
|
||||||
|
{ id: 'exp_insurance', name: '保険料', type: 'expense', normalBalance: 'debit', order: 85 },
|
||||||
|
{ id: 'exp_tax', name: '税金・社会保険料', type: 'expense', normalBalance: 'debit', order: 86 },
|
||||||
|
{ id: 'exp_tobacco', name: '嗜好品(タバコ等)', type: 'expense', normalBalance: 'debit', order: 87 },
|
||||||
|
{ id: 'exp_social', name: '交際費', type: 'expense', normalBalance: 'debit', order: 88 },
|
||||||
|
{ id: 'exp_savings', name: '貯蓄・積立', type: 'expense', normalBalance: 'debit', order: 89 },
|
||||||
|
{ id: 'exp_investment_out', name: '投資積立', type: 'expense', normalBalance: 'debit', order: 90 },
|
||||||
|
{ id: 'exp_other', name: 'その他支出', type: 'expense', normalBalance: 'debit', order: 91 },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// デフォルトカテゴリマッピング (大項目+中項目 → 費用/収益勘定)
|
||||||
|
// 中項目が空 = 大項目全体に適用
|
||||||
|
// ============================================================
|
||||||
|
export const DEFAULT_CATEGORY_MAPPINGS: CategoryMapping[] = [
|
||||||
|
// 収入
|
||||||
|
{ mfCategory: '収入', mfSubCategory: '給与', accountId: 'income_salary' },
|
||||||
|
{ mfCategory: '収入', mfSubCategory: '賞与', accountId: 'income_bonus' },
|
||||||
|
{ mfCategory: '収入', mfSubCategory: '一時所得', accountId: 'income_other' },
|
||||||
|
{ mfCategory: '収入', mfSubCategory: 'その他入金', accountId: 'income_other' },
|
||||||
|
{ mfCategory: '収入', mfSubCategory: '副収入', accountId: 'income_side' },
|
||||||
|
{ mfCategory: '収入', mfSubCategory: '', accountId: 'income_other' },
|
||||||
|
|
||||||
|
// ポイント・マイル
|
||||||
|
{ mfCategory: '収入', mfSubCategory: 'ポイント', accountId: 'income_point' },
|
||||||
|
{ mfCategory: 'ポイント', mfSubCategory: '', accountId: 'income_point' },
|
||||||
|
|
||||||
|
// 食費
|
||||||
|
{ mfCategory: '食費', mfSubCategory: '食料品', accountId: 'exp_food_grocery' },
|
||||||
|
{ mfCategory: '食費', mfSubCategory: '外食', accountId: 'exp_food_dining' },
|
||||||
|
{ mfCategory: '食費', mfSubCategory: 'カフェ', accountId: 'exp_food_dining' },
|
||||||
|
{ mfCategory: '食費', mfSubCategory: '', accountId: 'exp_food_grocery' },
|
||||||
|
|
||||||
|
// 日用品
|
||||||
|
{ mfCategory: '日用品', mfSubCategory: '', accountId: 'exp_daily' },
|
||||||
|
|
||||||
|
// 交通費
|
||||||
|
{ mfCategory: '交通費', mfSubCategory: '電車・バス', accountId: 'exp_transport' },
|
||||||
|
{ mfCategory: '交通費', mfSubCategory: 'タクシー', accountId: 'exp_transport' },
|
||||||
|
{ mfCategory: '交通費', mfSubCategory: '交通電子マネー', accountId: 'exp_transport' },
|
||||||
|
{ mfCategory: '交通費', mfSubCategory: '', accountId: 'exp_transport' },
|
||||||
|
|
||||||
|
// 自動車
|
||||||
|
{ mfCategory: '自動車', mfSubCategory: 'ガソリン', accountId: 'exp_car' },
|
||||||
|
{ mfCategory: '自動車', mfSubCategory: '', accountId: 'exp_car' },
|
||||||
|
{ mfCategory: '自動車・バイク', mfSubCategory: 'ガソリン代', accountId: 'exp_car' },
|
||||||
|
{ mfCategory: '自動車・バイク', mfSubCategory: '', accountId: 'exp_car' },
|
||||||
|
|
||||||
|
// 通信費
|
||||||
|
{ mfCategory: '通信費', mfSubCategory: '', accountId: 'exp_comms' },
|
||||||
|
|
||||||
|
// 光熱費
|
||||||
|
{ mfCategory: '水道・光熱費', mfSubCategory: '電気代', accountId: 'exp_electric' },
|
||||||
|
{ mfCategory: '水道・光熱費', mfSubCategory: 'ガス代', accountId: 'exp_gas' },
|
||||||
|
{ mfCategory: '水道・光熱費', mfSubCategory: '水道代', accountId: 'exp_water' },
|
||||||
|
{ mfCategory: '水道・光熱費', mfSubCategory: '', accountId: 'exp_electric' },
|
||||||
|
|
||||||
|
// 住居
|
||||||
|
{ mfCategory: '住居', mfSubCategory: '', accountId: 'exp_housing' },
|
||||||
|
{ mfCategory: '住宅', mfSubCategory: '', accountId: 'exp_housing' },
|
||||||
|
|
||||||
|
// 医療
|
||||||
|
{ mfCategory: '医療費', mfSubCategory: '', accountId: 'exp_medical' },
|
||||||
|
{ mfCategory: '健康・医療', mfSubCategory: '', accountId: 'exp_medical' },
|
||||||
|
|
||||||
|
// 娯楽
|
||||||
|
{ mfCategory: '娯楽', mfSubCategory: '', accountId: 'exp_entertainment' },
|
||||||
|
{ mfCategory: '趣味・娯楽', mfSubCategory: '', accountId: 'exp_entertainment' },
|
||||||
|
|
||||||
|
// サブスク
|
||||||
|
{ mfCategory: '教養・教育', mfSubCategory: 'サブスク', accountId: 'exp_subscription' },
|
||||||
|
{ mfCategory: '趣味・娯楽', mfSubCategory: 'サブスク', accountId: 'exp_subscription' },
|
||||||
|
|
||||||
|
// 書籍・教育
|
||||||
|
{ mfCategory: '教養・教育', mfSubCategory: '本・DVD', accountId: 'exp_book' },
|
||||||
|
{ mfCategory: '教養・教育', mfSubCategory: '', accountId: 'exp_book' },
|
||||||
|
|
||||||
|
// 衣服・美容
|
||||||
|
{ mfCategory: '衣服・美容', mfSubCategory: '', accountId: 'exp_clothing' },
|
||||||
|
|
||||||
|
// 保険
|
||||||
|
{ mfCategory: '保険', mfSubCategory: '', accountId: 'exp_insurance' },
|
||||||
|
|
||||||
|
// 税金・社会保障
|
||||||
|
{ mfCategory: '税金・社会保険', mfSubCategory: '', accountId: 'exp_tax' },
|
||||||
|
{ mfCategory: '税・社会保険', mfSubCategory: '', accountId: 'exp_tax' },
|
||||||
|
{ mfCategory: '税・社会保障', mfSubCategory: '', accountId: 'exp_tax' },
|
||||||
|
{ mfCategory: '税金・社会保障', mfSubCategory: '', accountId: 'exp_tax' },
|
||||||
|
{ mfCategory: '税金', mfSubCategory: '', accountId: 'exp_tax' },
|
||||||
|
|
||||||
|
// 交際費
|
||||||
|
{ mfCategory: '交際費', mfSubCategory: '', accountId: 'exp_social' },
|
||||||
|
|
||||||
|
// 未分類
|
||||||
|
{ mfCategory: '未分類', mfSubCategory: '', accountId: 'exp_other' },
|
||||||
|
|
||||||
|
// 水道・光熱費(実際のMFカテゴリに合わせて追加)
|
||||||
|
{ mfCategory: '水道・光熱費', mfSubCategory: 'ガス・灯油代', accountId: 'exp_gas' },
|
||||||
|
|
||||||
|
// タバコ・嗜好品
|
||||||
|
{ mfCategory: '嗜好品', mfSubCategory: 'タバコ', accountId: 'exp_tobacco' },
|
||||||
|
{ mfCategory: '嗜好品', mfSubCategory: '', accountId: 'exp_tobacco' },
|
||||||
|
|
||||||
|
// 現金・カード(資産間移動のためスキップ)
|
||||||
|
// ATM引き出し: 銀行→現金の振替、電子マネー: カード→Suicaのチャージ(実際の交通費は別途記録)
|
||||||
|
{ mfCategory: '現金・カード', mfSubCategory: 'ATM引き出し', accountId: '_skip_' },
|
||||||
|
{ mfCategory: '現金・カード', mfSubCategory: '電子マネー', accountId: '_skip_' },
|
||||||
|
{ mfCategory: '現金・カード', mfSubCategory: 'カード引き落とし', accountId: '_skip_' },
|
||||||
|
{ mfCategory: '現金・カード', mfSubCategory: '', accountId: '_skip_' },
|
||||||
|
|
||||||
|
// その他(資産間移動はスキップ)
|
||||||
|
{ mfCategory: 'その他', mfSubCategory: '自分の銀行へ移動', accountId: '_skip_' },
|
||||||
|
{ mfCategory: 'その他', mfSubCategory: '天引き貯金', accountId: 'exp_savings' },
|
||||||
|
{ mfCategory: 'その他', mfSubCategory: '投資用振り込み', accountId: 'exp_investment_out' },
|
||||||
|
{ mfCategory: 'その他', mfSubCategory: '', accountId: 'exp_other' },
|
||||||
|
{ mfCategory: '特別な支出', mfSubCategory: '', accountId: 'exp_other' },
|
||||||
|
{ mfCategory: 'その他支出', mfSubCategory: '', accountId: 'exp_other' },
|
||||||
|
{ mfCategory: '雑費', mfSubCategory: '', accountId: 'exp_other' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// デフォルト金融機関マッピング (保有金融機関 → 資産勘定)
|
||||||
|
// ============================================================
|
||||||
|
export const DEFAULT_INSTITUTION_MAPPINGS: InstitutionMapping[] = [
|
||||||
|
{ institution: '住信SBIネット銀行', accountId: 'bank_sbi' },
|
||||||
|
{ institution: 'SBI銀行', accountId: 'bank_sbi' },
|
||||||
|
{ institution: 'SBI証券', accountId: 'investment' },
|
||||||
|
{ institution: '三井住友銀行(Olive)', accountId: 'bank_smbc_olive' },
|
||||||
|
{ institution: '三菱UFJ銀行', accountId: 'bank_mufg' },
|
||||||
|
{ institution: '楽天銀行', accountId: 'bank_rakuten' },
|
||||||
|
{ institution: '楽天カード', accountId: 'credit_other' },
|
||||||
|
{ institution: 'モバイルSuica', accountId: 'suica' },
|
||||||
|
{ institution: 'VIEW CARD', accountId: 'credit_smbc' },
|
||||||
|
{ institution: '三井住友カード (VpassID)', accountId: 'credit_smbc' },
|
||||||
|
{ institution: 'Amazon.co.jp', accountId: 'credit_amazon' },
|
||||||
|
{ institution: '現金', accountId: 'cash' },
|
||||||
|
{ institution: 'リアルの個人財布', accountId: 'cash' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ヘルパー: カテゴリに対応する勘定科目IDを検索
|
||||||
|
// 中項目一致 → 大項目一致(中項目空) の順で優先
|
||||||
|
// ============================================================
|
||||||
|
export function findAccountByCategory(
|
||||||
|
mappings: CategoryMapping[],
|
||||||
|
category: string,
|
||||||
|
subCategory: string,
|
||||||
|
): string | null {
|
||||||
|
// 1. 大項目+中項目 完全一致
|
||||||
|
const exactMatch = mappings.find(
|
||||||
|
m => m.mfCategory === category && m.mfSubCategory === subCategory
|
||||||
|
);
|
||||||
|
if (exactMatch) return exactMatch.accountId;
|
||||||
|
|
||||||
|
// 2. 大項目一致・中項目空(大項目全体に適用)
|
||||||
|
const categoryMatch = mappings.find(
|
||||||
|
m => m.mfCategory === category && m.mfSubCategory === ''
|
||||||
|
);
|
||||||
|
if (categoryMatch) return categoryMatch.accountId;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// ヘルパー: 金融機関に対応する資産勘定科目IDを検索
|
||||||
|
// ============================================================
|
||||||
|
export function findAccountByInstitution(
|
||||||
|
mappings: InstitutionMapping[],
|
||||||
|
institution: string | undefined,
|
||||||
|
): string | null {
|
||||||
|
if (!institution) return null;
|
||||||
|
// 完全一致
|
||||||
|
const exact = mappings.find(m => m.institution === institution);
|
||||||
|
if (exact) return exact.accountId;
|
||||||
|
|
||||||
|
// 部分一致(前方)
|
||||||
|
const partial = mappings.find(m => institution.includes(m.institution) || m.institution.includes(institution));
|
||||||
|
if (partial) return partial.accountId;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 勘定科目種別の日本語ラベル
|
||||||
|
export const ACCOUNT_TYPE_LABELS: Record<string, string> = {
|
||||||
|
asset: '資産',
|
||||||
|
liability: '負債',
|
||||||
|
equity: '純資産',
|
||||||
|
income: '収益',
|
||||||
|
expense: '費用',
|
||||||
|
};
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import type { Account, Transaction, OpeningBalance } from '../types';
|
||||||
|
|
||||||
|
// 勘定科目の残高を計算(期首残高 + 仕訳の増減)
|
||||||
|
export function calcAccountBalance(
|
||||||
|
account: Account,
|
||||||
|
transactions: Transaction[],
|
||||||
|
openingBalances: OpeningBalance[],
|
||||||
|
): number {
|
||||||
|
// 期首残高
|
||||||
|
const opening = openingBalances.find(o => o.accountId === account.id)?.amount ?? 0;
|
||||||
|
|
||||||
|
// 仕訳合計
|
||||||
|
let debitTotal = 0;
|
||||||
|
let creditTotal = 0;
|
||||||
|
for (const tx of transactions) {
|
||||||
|
for (const entry of tx.entries) {
|
||||||
|
if (entry.accountId === account.id) {
|
||||||
|
debitTotal += entry.debit;
|
||||||
|
creditTotal += entry.credit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正規残高方向で計算
|
||||||
|
if (account.normalBalance === 'debit') {
|
||||||
|
return opening + debitTotal - creditTotal;
|
||||||
|
} else {
|
||||||
|
return opening + creditTotal - debitTotal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全勘定科目の残高マップを返す
|
||||||
|
export function calcAllBalances(
|
||||||
|
accounts: Account[],
|
||||||
|
transactions: Transaction[],
|
||||||
|
openingBalances: OpeningBalance[],
|
||||||
|
): Map<string, number> {
|
||||||
|
const map = new Map<string, number>();
|
||||||
|
for (const acc of accounts) {
|
||||||
|
map.set(acc.id, calcAccountBalance(acc, transactions, openingBalances));
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 指定期間の仕訳のみフィルタ
|
||||||
|
export function filterByMonth(
|
||||||
|
transactions: Transaction[],
|
||||||
|
year: number,
|
||||||
|
month: number, // 1-12
|
||||||
|
): Transaction[] {
|
||||||
|
const prefix = `${year}-${String(month).padStart(2, '0')}`;
|
||||||
|
return transactions.filter(tx => tx.date.startsWith(prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 月次損益を計算(収益合計 - 費用合計)
|
||||||
|
export function calcMonthlyPL(
|
||||||
|
accounts: Account[],
|
||||||
|
transactions: Transaction[],
|
||||||
|
year: number,
|
||||||
|
month: number,
|
||||||
|
): { incomeTotal: number; expenseTotal: number; netIncome: number; byAccount: Map<string, number> } {
|
||||||
|
const monthTx = filterByMonth(transactions, year, month);
|
||||||
|
const byAccount = new Map<string, number>();
|
||||||
|
|
||||||
|
let incomeTotal = 0;
|
||||||
|
let expenseTotal = 0;
|
||||||
|
|
||||||
|
for (const acc of accounts) {
|
||||||
|
if (acc.type !== 'income' && acc.type !== 'expense') continue;
|
||||||
|
const bal = calcAccountBalance(acc, monthTx, []);
|
||||||
|
byAccount.set(acc.id, bal);
|
||||||
|
if (acc.type === 'income') incomeTotal += bal;
|
||||||
|
if (acc.type === 'expense') expenseTotal += bal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { incomeTotal, expenseTotal, netIncome: incomeTotal - expenseTotal, byAccount };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 任意のtransactionsセットでP&Lを計算(全期間対応版)
|
||||||
|
export function calcPL(
|
||||||
|
accounts: Account[],
|
||||||
|
transactions: Transaction[],
|
||||||
|
): { incomeTotal: number; expenseTotal: number; netIncome: number; byAccount: Map<string, number> } {
|
||||||
|
const byAccount = new Map<string, number>();
|
||||||
|
let incomeTotal = 0;
|
||||||
|
let expenseTotal = 0;
|
||||||
|
|
||||||
|
for (const acc of accounts) {
|
||||||
|
if (acc.type !== 'income' && acc.type !== 'expense') continue;
|
||||||
|
const bal = calcAccountBalance(acc, transactions, []);
|
||||||
|
byAccount.set(acc.id, bal);
|
||||||
|
if (acc.type === 'income') incomeTotal += bal;
|
||||||
|
if (acc.type === 'expense') expenseTotal += bal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { incomeTotal, expenseTotal, netIncome: incomeTotal - expenseTotal, byAccount };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 月一覧を取得(データから)
|
||||||
|
export function getAvailableMonths(transactions: Transaction[]): string[] {
|
||||||
|
const months = new Set<string>();
|
||||||
|
for (const tx of transactions) {
|
||||||
|
months.add(tx.date.substring(0, 7)); // YYYY-MM
|
||||||
|
}
|
||||||
|
return Array.from(months).sort().reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 金額フォーマット
|
||||||
|
export function formatAmount(amount: number, showSign = false): string {
|
||||||
|
const formatted = Math.abs(amount).toLocaleString('ja-JP');
|
||||||
|
if (showSign && amount > 0) return `+¥${formatted}`;
|
||||||
|
if (amount < 0) return `-¥${formatted}`;
|
||||||
|
return `¥${formatted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// バランスシート構築
|
||||||
|
export interface BalanceSheetData {
|
||||||
|
assets: { account: Account; balance: number }[];
|
||||||
|
liabilities: { account: Account; balance: number }[];
|
||||||
|
equity: { account: Account; balance: number }[];
|
||||||
|
totalAssets: number;
|
||||||
|
totalLiabilities: number;
|
||||||
|
totalEquity: number;
|
||||||
|
netWorth: number; // 資産 - 負債
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildBalanceSheet(
|
||||||
|
accounts: Account[],
|
||||||
|
transactions: Transaction[],
|
||||||
|
openingBalances: OpeningBalance[],
|
||||||
|
targetDate?: string, // この日付以前の仕訳のみ
|
||||||
|
): BalanceSheetData {
|
||||||
|
const txs = targetDate
|
||||||
|
? transactions.filter(tx => tx.date <= targetDate)
|
||||||
|
: transactions;
|
||||||
|
|
||||||
|
// 収益-費用の差分(当期純利益)を純資産に加算するため計算
|
||||||
|
const incomeExpenseNet = accounts.reduce((net, acc) => {
|
||||||
|
if (acc.type !== 'income' && acc.type !== 'expense') return net;
|
||||||
|
const bal = calcAccountBalance(acc, txs, []);
|
||||||
|
return acc.type === 'income' ? net + bal : net - bal;
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const data: BalanceSheetData = {
|
||||||
|
assets: [],
|
||||||
|
liabilities: [],
|
||||||
|
equity: [],
|
||||||
|
totalAssets: 0,
|
||||||
|
totalLiabilities: 0,
|
||||||
|
totalEquity: 0,
|
||||||
|
netWorth: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const acc of accounts.filter(a => a.type === 'asset')) {
|
||||||
|
const bal = calcAccountBalance(acc, txs, openingBalances);
|
||||||
|
if (bal !== 0) {
|
||||||
|
data.assets.push({ account: acc, balance: bal });
|
||||||
|
data.totalAssets += bal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const acc of accounts.filter(a => a.type === 'liability')) {
|
||||||
|
const bal = calcAccountBalance(acc, txs, openingBalances);
|
||||||
|
if (bal !== 0) {
|
||||||
|
data.liabilities.push({ account: acc, balance: bal });
|
||||||
|
data.totalLiabilities += bal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const acc of accounts.filter(a => a.type === 'equity')) {
|
||||||
|
const bal = calcAccountBalance(acc, txs, openingBalances);
|
||||||
|
if (bal !== 0) {
|
||||||
|
data.equity.push({ account: acc, balance: bal });
|
||||||
|
data.totalEquity += bal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当期純利益を純資産に含める(仮勘定として表示)
|
||||||
|
if (incomeExpenseNet !== 0) {
|
||||||
|
data.equity.push({
|
||||||
|
account: {
|
||||||
|
id: '__net_income__',
|
||||||
|
name: '当期純利益',
|
||||||
|
type: 'equity',
|
||||||
|
normalBalance: 'credit',
|
||||||
|
order: 99,
|
||||||
|
},
|
||||||
|
balance: incomeExpenseNet,
|
||||||
|
});
|
||||||
|
data.totalEquity += incomeExpenseNet;
|
||||||
|
}
|
||||||
|
|
||||||
|
data.netWorth = data.totalAssets - data.totalLiabilities;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 総勘定元帳: 指定勘定の仕訳明細一覧
|
||||||
|
export interface LedgerLine {
|
||||||
|
date: string;
|
||||||
|
description: string;
|
||||||
|
counterAccountName: string; // 相手勘定
|
||||||
|
debit: number;
|
||||||
|
credit: number;
|
||||||
|
balance: number; // 累計残高
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildLedger(
|
||||||
|
account: Account,
|
||||||
|
accounts: Account[],
|
||||||
|
transactions: Transaction[],
|
||||||
|
openingBalance: number,
|
||||||
|
): LedgerLine[] {
|
||||||
|
const accountMap = new Map(accounts.map(a => [a.id, a]));
|
||||||
|
|
||||||
|
// 対象勘定の仕訳だけ抽出
|
||||||
|
type RawLine = { date: string; description: string; counterAccountId: string; debit: number; credit: number };
|
||||||
|
const lines: RawLine[] = [];
|
||||||
|
|
||||||
|
for (const tx of transactions) {
|
||||||
|
for (const entry of tx.entries) {
|
||||||
|
if (entry.accountId !== account.id) continue;
|
||||||
|
// 相手勘定を探す
|
||||||
|
const counterEntry = tx.entries.find(e => e.accountId !== account.id);
|
||||||
|
lines.push({
|
||||||
|
date: tx.date,
|
||||||
|
description: tx.description,
|
||||||
|
counterAccountId: counterEntry?.accountId ?? '',
|
||||||
|
debit: entry.debit,
|
||||||
|
credit: entry.credit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 日付順
|
||||||
|
lines.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|
||||||
|
// 累計残高を計算
|
||||||
|
let balance = openingBalance;
|
||||||
|
return lines.map(line => {
|
||||||
|
const counterName = accountMap.get(line.counterAccountId)?.name ?? '';
|
||||||
|
if (account.normalBalance === 'debit') {
|
||||||
|
balance += line.debit - line.credit;
|
||||||
|
} else {
|
||||||
|
balance += line.credit - line.debit;
|
||||||
|
}
|
||||||
|
return { ...line, counterAccountName: counterName, balance };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import Papa from 'papaparse';
|
||||||
|
import type { MFRecord, Transaction, CategoryMapping, InstitutionMapping, Account, TransferOverride } from '../types';
|
||||||
|
import { findAccountByCategory, findAccountByInstitution } from './accounts';
|
||||||
|
|
||||||
|
// MoneyForward CSV を Shift-JIS でデコードしてパース
|
||||||
|
export async function parseMFCsv(file: File): Promise<MFRecord[]> {
|
||||||
|
const buffer = await file.arrayBuffer();
|
||||||
|
const decoder = new TextDecoder('shift-jis');
|
||||||
|
const text = decoder.decode(buffer);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Papa.parse<Record<string, string>>(text, {
|
||||||
|
header: true,
|
||||||
|
skipEmptyLines: true,
|
||||||
|
transformHeader: (header: string) => header.trim(),
|
||||||
|
complete: (results) => {
|
||||||
|
const records: MFRecord[] = results.data.map((row) => {
|
||||||
|
// 金額列: "金額(円)" または "金額" として読み込まれる
|
||||||
|
const amountStr = row['金額(円)'] ?? row['金額'] ?? '0';
|
||||||
|
const amount = parseInt(amountStr.replace(/,/g, ''), 10) || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
計算対象: row['計算対象'] ?? '',
|
||||||
|
日付: (row['日付'] ?? '').trim(),
|
||||||
|
内容: (row['内容'] ?? '').trim(),
|
||||||
|
金額: amount,
|
||||||
|
保有金融機関: (row['保有金融機関'] ?? '').trim(),
|
||||||
|
大項目: (row['大項目'] ?? '').trim(),
|
||||||
|
中項目: (row['中項目'] ?? '').trim(),
|
||||||
|
メモ: (row['メモ'] ?? '').trim(),
|
||||||
|
振替: (row['振替'] ?? '0').trim(),
|
||||||
|
ID: (row['ID'] ?? '').trim(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
resolve(records);
|
||||||
|
},
|
||||||
|
error: reject,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// MoneyForwardの日付 "YYYY/MM/DD" → "YYYY-MM-DD"
|
||||||
|
function normalizeDate(mfDate: string): string {
|
||||||
|
return mfDate.replace(/\//g, '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
// MFRecordのリストを複式仕訳に変換
|
||||||
|
// カテゴリマッピングで解決した勘定科目のタイプ(expense/income)で借方・貸方を決定する。
|
||||||
|
// MFのCSVでは現金支出が正の金額で記録される場合があるため、金額の符号ではなく
|
||||||
|
// 勘定科目タイプを優先して判断する。
|
||||||
|
export function convertToTransactions(
|
||||||
|
records: MFRecord[],
|
||||||
|
categoryMappings: CategoryMapping[],
|
||||||
|
institutionMappings: InstitutionMapping[],
|
||||||
|
existingIds: Set<string>,
|
||||||
|
accounts: Account[] = [],
|
||||||
|
transferOverrides: TransferOverride[] = [],
|
||||||
|
): { transactions: Transaction[]; updated: number; unmapped: MFRecord[] } {
|
||||||
|
const accountMap = new Map(accounts.map(a => [a.id, a]));
|
||||||
|
const overrideMap = new Map(transferOverrides.map(o => [o.mfId, o]));
|
||||||
|
const transactions: Transaction[] = [];
|
||||||
|
const unmapped: MFRecord[] = [];
|
||||||
|
let updated = 0;
|
||||||
|
|
||||||
|
for (const rec of records) {
|
||||||
|
// 振替取引(口座間移動)は原則スキップ。ただし除外設定があれば収入/費用として処理
|
||||||
|
if (rec.振替 === '1') {
|
||||||
|
// ID完全一致 → 内容テキスト部分一致 の順でマッチ
|
||||||
|
const override = overrideMap.get(rec.ID)
|
||||||
|
?? transferOverrides.find(o => o.description && rec.内容.includes(o.description));
|
||||||
|
if (!override) continue;
|
||||||
|
|
||||||
|
// 除外設定あり: 指定勘定科目への収入として処理
|
||||||
|
if (rec.ID && existingIds.has(rec.ID)) updated++;
|
||||||
|
const date = normalizeDate(rec.日付);
|
||||||
|
const amount = Math.abs(rec.金額);
|
||||||
|
const assetAccountId = findAccountByInstitution(institutionMappings, rec.保有金融機関) ?? 'other_asset';
|
||||||
|
const overrideAccount = accountMap.get(override.accountId);
|
||||||
|
const isExpense = overrideAccount?.type === 'expense';
|
||||||
|
transactions.push({
|
||||||
|
id: rec.ID || `${date}-${rec.内容}-${rec.金額}`,
|
||||||
|
date,
|
||||||
|
description: rec.内容,
|
||||||
|
entries: isExpense
|
||||||
|
? [
|
||||||
|
{ accountId: override.accountId, debit: amount, credit: 0 },
|
||||||
|
{ accountId: assetAccountId, debit: 0, credit: amount },
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
{ accountId: assetAccountId, debit: amount, credit: 0 },
|
||||||
|
{ accountId: override.accountId, debit: 0, credit: amount },
|
||||||
|
],
|
||||||
|
mfInstitution: rec.保有金融機関,
|
||||||
|
mfCategory: rec.大項目,
|
||||||
|
mfSubCategory: rec.中項目,
|
||||||
|
mfMemo: rec.メモ,
|
||||||
|
mfId: rec.ID,
|
||||||
|
isTransfer: false,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 既存IDは更新カウント(スキップせず上書き)
|
||||||
|
if (rec.ID && existingIds.has(rec.ID)) updated++;
|
||||||
|
|
||||||
|
const date = normalizeDate(rec.日付);
|
||||||
|
const amount = rec.金額;
|
||||||
|
if (amount === 0) continue;
|
||||||
|
|
||||||
|
// カテゴリ → 勘定科目
|
||||||
|
const categoryAccountId = findAccountByCategory(categoryMappings, rec.大項目, rec.中項目);
|
||||||
|
if (!categoryAccountId) {
|
||||||
|
unmapped.push(rec);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (categoryAccountId === '_skip_') continue;
|
||||||
|
|
||||||
|
// 金融機関 → 資産勘定
|
||||||
|
const assetAccountId = findAccountByInstitution(institutionMappings, rec.保有金融機関)
|
||||||
|
?? 'other_asset';
|
||||||
|
|
||||||
|
// 勘定科目タイプで借方・貸方を決定(金額の符号より優先)
|
||||||
|
const categoryAccount = accountMap.get(categoryAccountId);
|
||||||
|
const isExpense = categoryAccount
|
||||||
|
? categoryAccount.type === 'expense'
|
||||||
|
: amount < 0; // accounts未渡しのフォールバック
|
||||||
|
|
||||||
|
if (isExpense) {
|
||||||
|
// 費用: 借方=費用勘定, 貸方=資産勘定(現金・口座から支払い)
|
||||||
|
transactions.push({
|
||||||
|
id: rec.ID || `${date}-${rec.内容}-${amount}`,
|
||||||
|
date,
|
||||||
|
description: rec.内容,
|
||||||
|
entries: [
|
||||||
|
{ accountId: categoryAccountId, debit: Math.abs(amount), credit: 0 },
|
||||||
|
{ accountId: assetAccountId, debit: 0, credit: Math.abs(amount) },
|
||||||
|
],
|
||||||
|
mfInstitution: rec.保有金融機関,
|
||||||
|
mfCategory: rec.大項目,
|
||||||
|
mfSubCategory: rec.中項目,
|
||||||
|
mfMemo: rec.メモ,
|
||||||
|
mfId: rec.ID,
|
||||||
|
isTransfer: false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 収益: 借方=資産勘定, 貸方=収益勘定(現金・口座に入金)
|
||||||
|
transactions.push({
|
||||||
|
id: rec.ID || `${date}-${rec.内容}-${amount}`,
|
||||||
|
date,
|
||||||
|
description: rec.内容,
|
||||||
|
entries: [
|
||||||
|
{ accountId: assetAccountId, debit: Math.abs(amount), credit: 0 },
|
||||||
|
{ accountId: categoryAccountId, debit: 0, credit: Math.abs(amount) },
|
||||||
|
],
|
||||||
|
mfInstitution: rec.保有金融機関,
|
||||||
|
mfCategory: rec.大項目,
|
||||||
|
mfSubCategory: rec.中項目,
|
||||||
|
mfMemo: rec.メモ,
|
||||||
|
mfId: rec.ID,
|
||||||
|
isTransfer: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { transactions, updated, unmapped };
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 76 KiB |
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
||||||
|
|
||||||
|
const DATA_FILE = path.resolve('./data.json')
|
||||||
|
|
||||||
|
function readData(): Record<string, unknown> {
|
||||||
|
if (!fs.existsSync(DATA_FILE)) return {}
|
||||||
|
try {
|
||||||
|
return JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'))
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readBody(req: IncomingMessage): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let body = ''
|
||||||
|
req.on('data', (chunk: Buffer) => { body += chunk.toString() })
|
||||||
|
req.on('end', () => resolve(body))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
{
|
||||||
|
name: 'data-api',
|
||||||
|
configureServer(server) {
|
||||||
|
server.middlewares.use('/api/data', async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const data = readData()
|
||||||
|
res.end(JSON.stringify(data))
|
||||||
|
|
||||||
|
} else if (req.method === 'POST') {
|
||||||
|
const body = await readBody(req)
|
||||||
|
const incoming = JSON.parse(body) as { key: string; value: unknown }
|
||||||
|
const data = readData()
|
||||||
|
data[incoming.key] = incoming.value
|
||||||
|
fs.writeFileSync(DATA_FILE, JSON.stringify(data))
|
||||||
|
res.end('{"ok":true}')
|
||||||
|
|
||||||
|
} else if (req.method === 'DELETE') {
|
||||||
|
const key = new URL(req.url ?? '', 'http://x').searchParams.get('key')
|
||||||
|
if (key) {
|
||||||
|
const data = readData()
|
||||||
|
delete data[key]
|
||||||
|
fs.writeFileSync(DATA_FILE, JSON.stringify(data))
|
||||||
|
}
|
||||||
|
res.end('{"ok":true}')
|
||||||
|
|
||||||
|
} else {
|
||||||
|
res.statusCode = 405
|
||||||
|
res.end('{"error":"Method Not Allowed"}')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
server: {
|
||||||
|
host: true, // ローカルネットワーク全体に公開
|
||||||
|
},
|
||||||
|
})
|
||||||