無題

Pasted Oct 11, 2026, 8:16 PM1 file8,967 lines

Scan to open on your phone

Point a camera at the QR code - it opens this exact paste, no app needed.

https://codepastes.com/zzgsykgtf3gzae57
無題
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>支出管理</title>

<style>
:root {
    --main-color: #4f6f52;
    --main-color-light: #e9f0e9;
    --card-color: rgba(255,255,255,var(--card-opacity,.94));
    --text-color: #222;
    --sub-text: #666;
    --border-color: #d8ddd8;
    --danger: #d93025;

--heading-size: 20px;
--year-size: 15px;
--month-size: 15px;
--add-size: 15px;
--setting-size: 15px;
--description-size: 16px;

    --shadow:
        0 4px 18px rgba(0,0,0,.08);
}

* {
    box-sizing: border-box;
}

html,
body {
    margin: 0;
    padding: 0;
    min-height: 100%;
    font-family:
        -apple-system,
        BlinkMacSystemFont,
        "Segoe UI",
        "Hiragino Kaku Gothic ProN",
        "Yu Gothic",
        Meiryo,
        sans-serif;
    color: var(--text-color);
background: transparent;
}

body {
    min-height: 100vh;
    background-size: cover;
    background-position: center;
    background-attachment: fixed;
}

body::before {
    content: "";
    position: fixed;
    inset: 0;
    pointer-events: none;
    z-index: 0;

    background-image: var(--background-image, none);
    background-size: cover;
    background-position: center;
    background-attachment: fixed;
    opacity: 1;
}

body.dark {
        --card-color: rgba(32,35,32,var(--card-opacity,.95));
        --text-color: #eee;
        --sub-text: #aaa;
        --border-color: #454a45;
        --main-color-light: #29372b;
    }

button,
input,
textarea,
select {
    font: inherit;
}

button {
    cursor: pointer;
}

.hidden {
    display: none !important;
}

/* =========================================================
   起動画面
========================================================= */

#startupScreen {
    min-height: 100vh;

    display: flex;
    justify-content: center;
    align-items: center;

    padding: 30px;

    position: relative;
    z-index: 1;

    overflow: hidden;

    background:
        radial-gradient(
            circle at 12% 18%,
            #a9e6bd 0%,
            transparent 40%
        ),
        radial-gradient(
            circle at 88% 78%,
            #aec9f2 0%,
            transparent 42%
        ),
        radial-gradient(
            circle at 55% 105%,
            #f5d99a 0%,
            transparent 44%
        ),
        linear-gradient(
            135deg,
            #e1f4e6 0%,
            #e3ebfa 48%,
            #f8eedb 100%
        );
}

#startupScreen::before {
    content: "";

    position: absolute;

    width: 500px;
    height: 500px;

    top: -230px;
    right: -150px;

    border-radius: 50%;

    background:
        radial-gradient(
            circle,
            rgba(255,255,255,.55) 0%,
            rgba(255,255,255,.18) 45%,
            transparent 70%
        );

    pointer-events: none;
}

#startupScreen::after {
    content: "";

    position: absolute;

    width: 420px;
    height: 420px;

    bottom: -220px;
    left: -140px;

    border-radius: 50%;

    background:
        radial-gradient(
            circle,
            rgba(255,255,255,.40) 0%,
            rgba(255,255,255,.10) 45%,
            transparent 72%
        );

    pointer-events: none;
}

.startup-card {
    width: min(560px, 100%);
    background: var(--card-color);
    border: 1px solid var(--border-color);
    border-radius: 20px;
    padding: 40px;
    box-shadow: var(--shadow);
    text-align: center;

    position: relative;
    z-index: 2;
}

.startup-title {
    font-size: 32px;
    font-weight: 700;
    margin-bottom: 12px;
}

.startup-description {
    color: var(--sub-text);
    line-height: 1.8;
    margin-bottom: 30px;
}

.folder-status {
    background: var(--main-color-light);
    border-radius: 12px;
    padding: 15px;
    margin-bottom: 20px;
    font-size: 14px;
    text-align: left;
}

/* =========================================================
   ボタン
========================================================= */

.primary-btn,
.secondary-btn,
.danger-btn {
    border: none;
    border-radius: 9px;
    padding: 10px 18px;
    font-weight: 600;
}

.primary-btn {
    background: var(--main-color);
    color: white;
}

.secondary-btn {
    background: var(--main-color-light);
    color: var(--text-color);
}

.danger-btn {
    background: var(--danger);
    color: white;
}

.primary-btn:hover,
.secondary-btn:hover,
.danger-btn:hover {
    opacity: .88;
}

/* =========================================================
   上部バー
========================================================= */

.topbar {
    position: sticky;
    top: 0;
    z-index: 100;

    min-height: 64px;

    display: flex;
    align-items: center;
    gap: 4px;

    padding: 0 18px;

    background: var(--card-color);
    border-bottom: 1px solid var(--border-color);

    backdrop-filter: blur(12px);
}

.app-title {
    font-size: var(--heading-size);
    font-weight: 700;
    margin-right: 15px;
    white-space: nowrap;
}

.nav-btn {
    position: relative;

    border: none;
    background: transparent;
    color: var(--text-color);

    padding: 10px 13px;
    border-radius: 8px;

    transition:
        background .15s,
        color .15s;
}

/* ホバー */
.nav-btn:hover {
    background: var(--main-color-light);
}

/* 選択中 */
.nav-btn.active {
    background: var(--main-color);
    color: white;
    font-weight: 700;
}

/*
   選択中であることをさらに分かりやすくする
*/
.nav-btn.active::after {
    content: "";
    position: absolute;

    left: 10px;
    right: 10px;
    bottom: -5px;

    height: 3px;

    background: var(--main-color);
    border-radius: 10px;
}

.nav-spacer {
    flex: 1;
}

.folder-indicator {
    font-size: 12px;
    color: var(--sub-text);

    max-width: 200px;

    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

    margin-right: 5px;
}

/* =========================================================
   本体
========================================================= */

#app {
    min-height: 100vh;
    position: relative;
    z-index: 1;
}

.main {
    max-width: 1500px;
    margin: 0 auto;
    padding: 28px;
}

.page-title {
    font-size: 27px;
    font-weight: 700;
    margin: 0 0 22px;
}

.page-description {
    color: var(--sub-text);
    margin-bottom: 20px;
    line-height: 1.8;
}

/* =========================================================
   年別
========================================================= */

.year-table-wrapper {
    width: 100%;
    height: calc(100vh - 200px);
    overflow: auto;

    background: var(--card-color);
    border: 1px solid var(--border-color);
    border-radius: 14px;

    box-shadow: var(--shadow);
}

.year-table {
    width: 100%;
    border-collapse: collapse;
    min-width: 1000px;
font-size: var(--year-size);
}

.year-table th,
.list-table th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: #e9f0e9;
    color: #222;
    font-weight: 700;
}

body.dark .list-table th {
    background: #29372b;
    color: #eee;
}

.year-table th {
    background: #e9f0e9 !important;
    background-color: #e9f0e9 !important;
    background-image: none !important;
    font-weight: 700;
    opacity: 1 !important;
    position: sticky;
    top: 0;
    z-index: 20;
    isolation: isolate;
}

body.dark .year-table th {
    background: #29372b !important;
    background-color: #29372b !important;
    color: #eee !important;
}

.year-table th:first-child,
.year-table td:first-child {
    text-align: left;
}

 .year-table td {
        border-bottom: 1px solid var(--border-color);
        padding: 8px 10px;
    }

/* 年別画面:数値を右揃えにし、行間を広げる */
.year-table th,
.year-table td {
    padding: 16px 18px;
    text-align: right;
    vertical-align: middle;
}

/* 年の項目名は左揃え */
.year-table th:first-child,
.year-table td:first-child {
    text-align: left;
}

/* 年別画面:前年比を3段に表示 */
.year-change {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 9px;
    margin-top: 9px;
    font-size: 0.85em;
    line-height: 1.7;
    text-align: right;
}

.year-change-rate,
.year-change-amount {
    display: block;
    white-space: nowrap;
    text-align: right;
}

.year-change-amount {
    margin-bottom: 4px;
}

.year-table tr:last-child td {
    border-bottom: none;
}

.year-link {
    color: var(--main-color);
    font-weight: 700;
    cursor: pointer;
    text-decoration: underline;
}

.memo-edit-btn {
    border: 1px solid var(--border-color);
    background: transparent;
    color: var(--text-color);

    border-radius: 7px;
    padding: 6px 10px;
}

.memo-status {
    display: inline-flex;
    align-items: center;
    margin-right: 8px;
}

.memo-status input {
    width: 18px;
    height: 18px;
    margin: 0;
    accent-color: var(--main-color);
    pointer-events: none;
}

/*
   前年比
*/
.year-change {
    margin-left: 4px;
    font-size: .82em;
    color: var(--sub-text);
}


.year-change.negative {
    color: var(--danger);
    font-weight: 700;
}

/* =========================================================
   詳細
========================================================= */

.detail-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 22px;
}

.detail-year {
    font-size: 36px;
    font-weight: 800;
    color: var(--main-color);
}

.back-btn {
    border: 1px solid var(--border-color);
    background: var(--card-color);

    border-radius: 9px;
    padding: 9px 14px;
}

.month-table-wrapper {
    overflow-x: auto;

    background: var(--card-color);
    border: 1px solid var(--border-color);
    border-radius: 14px;

    box-shadow: var(--shadow);
}

.month-table {
        width: 100%;
        min-width: max-content;
        border-collapse: collapse;
        table-layout: auto;
        font-size: var(--month-size);
    }

    .month-table th,
    .month-table td {
        padding: 0.35em 0.4em;
        border-bottom: 1px solid var(--border-color);
        text-align: right;
        vertical-align: top;
    }

    .month-table th {
        padding: 11px 10px;
        background: var(--main-color-light);
        color: var(--text-color);
        white-space: nowrap;
    }

    .month-table th:first-child,
    .month-table td:first-child {
        text-align: left;
    }

.asset-cell {
    font-weight: 700;
    background: rgba(79,111,82,.08);
}

.sub-section-title {
    font-size: 20px;
    font-weight: 700;
    margin: 35px 0 15px;
}

/* =========================================================
   明細
========================================================= */

.detail-list {
    display: flex;
    flex-direction: column;
    gap: 5px;

    min-width: 150px;

    text-align: left;
}

.detail-line {
    display: flex;
    justify-content: space-between;

    gap: 15px;
}

.detail-note {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

    max-width: 170px;
}

.detail-empty {
    color: var(--sub-text);
}

/* =========================================================
   追加・編集画面
========================================================= */

.form-card {
    background: var(--card-color);

    border: 1px solid var(--border-color);
    border-radius: 14px;

    padding: 22px;

    box-shadow: var(--shadow);

    margin-bottom: 20px;

font-size: var(--add-size);
}

/*
   追加画面は横幅を有効利用する
*/
.add-form-grid {
    display: grid;

    grid-template-columns:
        repeat(4, minmax(140px, 1fr));

    gap: 12px 14px;
}

.form-group {
    display: flex;
    flex-direction: column;
    gap: 6px;

    min-width: 0;
}

.form-group label {
    font-weight: 600;
    font-size: .92em;
}

.form-group input,
.form-group textarea {
    width: 100%;
}

/*
   各段の幅を制御
*/
.form-row-full {
    grid-column: 1 / -1;
}

/*
   年月
*/
.form-year-month {
    grid-column: span 1;
}

/*
   臨時収入・出費
*/
.detail-editor-section {
    margin-top: 20px;
}

.detail-editor-title {
    font-size: 17px;
    font-weight: 700;
    margin-bottom: 8px;
}

.line-editor {
    display: flex;
    flex-direction: column;
    gap: 7px;
}

/*
   明細1行
*/
.line-row {
    display: grid;

    grid-template-columns:
        minmax(0, 1fr)
        150px
        auto;

    gap: 7px;

    align-items: center;
}

.line-row button {
    border: none;

    background: #eee;
    color: #333;

    border-radius: 7px;

    padding: 8px 11px;
}

body.dark .line-row button {
    background: #444;
    color: white;
}

/*
   「追加」は削除の左隣
*/
.line-actions {
    display: flex;
    gap: 5px;
    align-items: center;
}

.add-line-small,
.delete-line-small {
    border-radius: 7px;

    padding: 8px 12px;

    white-space: nowrap;
}

.add-line-small {
    border: 1px solid var(--main-color);

    background: var(--main-color);
    color: white;
}

.delete-line-small {
    border: 1px solid var(--border-color);

    background: #eee;
    color: #333;
}

body.dark .delete-line-small {
    background: #444;
    color: white;
}

/*
   資産・ローン・積立
*/
.three-column-row {
    display: grid;

    grid-template-columns:
        repeat(4, minmax(0, 1fr));

    gap: 14px;
}

/*
   電気・ガス・水道
*/
.four-column-row {
    display: grid;

    grid-template-columns:
        repeat(4, minmax(0, 1fr));

    gap: 14px;
}

.utility-inner {
    display: grid;

    grid-template-columns:
        1fr 1fr;

    gap: 7px;
}

/*
   メモ
*/
.memo-area {
    margin-top: 20px;
}

.memo-area textarea {
    min-height: 100px;
    resize: vertical;
}

.form-actions {
    display: flex;

    justify-content: flex-end;

    gap: 10px;

    margin-top: 25px;
}

/* =========================================================
   設定
========================================================= */

.setting-section {
    background: var(--card-color);

    border: 1px solid var(--border-color);
    border-radius: 14px;

    padding: 24px;

    margin-bottom: 18px;

    box-shadow: var(--shadow);
font-size: var(--setting-size);
}

.setting-section h3 {
    margin-top: 0;
    font-size: var(--setting-size) !important;
}

.setting-row {
    display: flex;

    align-items: center;
    justify-content: space-between;

    gap: 20px;

    padding: 12px 0;

    border-bottom: 1px solid var(--border-color);
}

.setting-row:last-child {
    border-bottom: none;
}

.range-value {
    min-width: 50px;
    text-align: right;
}

.setting-control {
    display: flex;
    align-items: center;
    gap: 10px;
}

 /* フォント選択欄と文字サイズ表示の間隔 */
    .setting-control select[id^="font-family-"] {
        width: auto;
        min-width: 170px;
        margin-left: 12px;
        flex-shrink: 0;
    }

/* =========================================================
   説明
========================================================= */

.description-card {
    background: var(--card-color);

    border: 1px solid var(--border-color);
    border-radius: 14px;

    padding: 30px;

    line-height: 2;

    font-size: var(--description-size);

    box-shadow: var(--shadow);
}

.description-card h2 {
    margin-top: 0;
    font-family: var(--description-font) !important;
    font-size: var(--description-size) !important;
}

/* =========================================================
   モーダル
========================================================= */

.modal {
    position: fixed;
    inset: 0;

    background: rgba(0,0,0,.48);

    display: flex;
    justify-content: center;
    align-items: center;

    padding: 20px;

    z-index: 500;
}

.modal-card {
    width: min(720px, 100%);
    max-height: 90vh;

    overflow: auto;

    background: var(--card-color);
    color: var(--text-color);

    border-radius: 15px;

    padding: 25px;

    box-shadow:
        0 15px 50px rgba(0,0,0,.25);
}

.modal-title {
    font-size: 21px;
    font-weight: 700;
    margin-bottom: 18px;
}

.modal-actions {
    display: flex;

    justify-content: flex-end;

    gap: 8px;

    margin-top: 18px;
}

.warning {
    color: var(--danger);
    font-weight: 700;
}

/* =========================================================
   入力
========================================================= */

input[type="text"],
input[type="number"],
input[type="month"],
textarea,
select {
    width: 100%;

    border: 1px solid var(--border-color);
    border-radius: 8px;

    padding: 9px 10px;

    background: var(--card-color);
    color: var(--text-color);
}

/* =========================================================
   設定画面の選択ボタン
========================================================= */

#darkModeSetting {
    width: 58px;
    height: 32px;

    appearance: none;
    -webkit-appearance: none;

    position: relative;

    border: none;
    border-radius: 999px;

    background: #cfd5cf;

    cursor: pointer;

    transition:
        background .2s,
        box-shadow .2s;
}

#darkModeSetting::after {
    content: "";

    position: absolute;

    top: 4px;
    left: 4px;

    width: 24px;
    height: 24px;

    border-radius: 50%;

    background: white;

    box-shadow:
        0 2px 5px rgba(0,0,0,.22);

    transition:
        transform .2s;
}

#darkModeSetting:checked {
    background: var(--main-color);

    box-shadow:
        0 0 0 3px var(--main-color-light);
}

#darkModeSetting:checked::after {
    transform: translateX(26px);
}

#mainColorSetting,
#backgroundColorSetting {
    width: 64px;
    height: 42px;

    padding: 4px;

    border: 2px solid var(--border-color);
    border-radius: 10px;

    background: var(--card-color);

    cursor: pointer;

    box-shadow:
        0 2px 8px rgba(0,0,0,.10);

    transition:
        transform .15s,
        box-shadow .15s,
        border-color .15s;
}

#mainColorSetting:hover,
#backgroundColorSetting:hover {
    transform: translateY(-1px);

    border-color: var(--main-color);

    box-shadow:
        0 4px 12px rgba(0,0,0,.16);
}

#mainColorSetting:focus,
#backgroundColorSetting:focus {
    outline: none;

    border-color: var(--main-color);

    box-shadow:
        0 0 0 3px var(--main-color-light),
        0 4px 12px rgba(0,0,0,.12);
}

textarea {
    min-height: 100px;
}

/* =========================================================
   レスポンシブ
========================================================= */

@media (max-width: 1000px) {

    .add-form-grid {
        grid-template-columns:
            repeat(2, minmax(0, 1fr));
    }

    .three-column-row {
        grid-template-columns:
            repeat(2, minmax(0, 1fr));
    }

    .four-column-row {
        grid-template-columns:
            repeat(2, minmax(0, 1fr));
    }
}

@media (max-width: 700px) {

    .topbar {
        overflow-x: auto;
    }

    .folder-indicator {
        display: none;
    }

    .main {
        padding: 18px 12px;
    }

    .add-form-grid,
    .three-column-row,
    .four-column-row {
        grid-template-columns: 1fr;
    }

    .line-row {
        grid-template-columns: 1fr;
    }

    .line-actions {
        justify-content: flex-start;
    }

    .detail-year {
        font-size: 28px;
    }
}

/* =========================================================
   一覧ページ
========================================================= */

.list-toolbar {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 12px;
}

.list-toolbar button {
    border: 1px solid var(--border-color);
    border-radius: 7px;
    padding: 7px 12px;
    background: var(--card-color);
    color: var(--text-color);
}

.list-toolbar button.active {
    background: var(--main-color);
    color: white;
}

.list-year-panel {
    display: flex;
    flex-wrap: wrap;
    gap: 7px 14px;
    padding: 12px;
    margin-bottom: 12px;
    border: 1px solid var(--border-color);
    border-radius: 9px;
    background: var(--card-color);
}

.list-year-panel label {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}

.list-table-wrapper {
    width: 100%;
    overflow: auto;
    max-height: calc(100vh - 230px);
    border: 1px solid var(--border-color);
    border-radius: 9px;
    background: var(--card-color);
}

.list-table {
    width: max-content;
    min-width: 100%;
    border-collapse: collapse;
    table-layout: auto;
}

.list-table th,
.list-table td {
    border: 1px solid var(--border-color);
    white-space: nowrap;
    text-align: right;
    vertical-align: middle;
}

.list-table th {
    position: sticky;
    top: 0;
    z-index: 2;
    background: var(--main-color-light);
    color: var(--text-color);
    font-weight: 700;
}

.list-table th:first-child,
.list-table td:first-child {
    position: sticky;
    left: 0;
    text-align: left;
    background-color: var(--card-color);
    color: var(--text-color);
    z-index: 1;
}

.list-table th:first-child {
    background-color: var(--main-color-light);
    color: var(--text-color);
    z-index: 4;
}

.list-table .list-memo-cell {
    max-width: 160px;
    overflow: hidden;
    text-overflow: ellipsis;
    text-align: left;
}

.list-table .list-detail-memo {
    text-align: center;
    min-width: 30px;
}

.list-memo-button {
    border: 0;
    padding: 1px 5px;
    background: transparent;
    color: var(--text-color);
    cursor: pointer;
    font-weight: 700;
}

.list-bonus {
    color: red !important;
    font-weight: 700;
}

.list-empty {
    text-align: center !important;
    padding: 25px;
    color: var(--sub-text);
}

/* サイズ1:かなり小さい */
.list-size-1 .list-table {
    font-size: 8px;
}
.list-size-1 .list-table th,
.list-size-1 .list-table td {
    padding: 2px 3px;
}

/* サイズ2 */
.list-size-2 .list-table {
    font-size: 9px;
}
.list-size-2 .list-table th,
.list-size-2 .list-table td {
    padding: 3px 4px;
}

/* サイズ3 */
.list-size-3 .list-table {
    font-size: 10px;
}
.list-size-3 .list-table th,
.list-size-3 .list-table td {
    padding: 4px 5px;
}

/* サイズ4:標準 */
.list-size-4 .list-table {
    font-size: 12px;
}
.list-size-4 .list-table th,
.list-size-4 .list-table td {
    padding: 6px 7px;
}

/* サイズ5:大きめ */
.list-size-5 .list-table {
    font-size: 14px;
}
.list-size-5 .list-table th,
.list-size-5 .list-table td {
    padding: 8px 9px;
}

/* 文字設定:フォントの適用 */
:root {
    --heading-font: "Yu Gothic", YuGothic, sans-serif;
    --year-font: "Yu Gothic", YuGothic, sans-serif;
    --month-font: "Yu Gothic", YuGothic, sans-serif;
    --add-font: "Yu Gothic", YuGothic, sans-serif;
    --setting-font: "Yu Gothic", YuGothic, sans-serif;
    --description-font: "Yu Gothic", YuGothic, sans-serif;
}

.app-title,
.startup-title,
.topbar .nav-btn,
.topbar .folder-indicator,
.page-title,
.sub-section-title,
.detail-editor-title,
.modal-title {
    font-family: var(--heading-font);
}

.year-table-wrapper {
    font-family: var(--year-font);
}

.month-table-wrapper {
    font-family: var(--month-font);
}

.form-card {
    font-family: var(--add-font);
}

.setting-section {
    font-family: var(--setting-font);
}

.description-card {
    font-family: var(--description-font);
}

/* 文字設定:画面見出しと上部メニュー */
h1,
h2,
h3,
h4,
h5,
h6,
.page-title,
.sub-section-title,
.detail-editor-title,
.modal-title,
.nav-btn,
.folder-indicator {
    font-size: var(--heading-size) !important;
}

/* アプリ説明内は「説明」のフォント・文字サイズを優先 */
.description-font {
    font-family: var(--description-font) !important;
    font-size: var(--description-size) !important;
}

/* ODSバックアップページは「説明」の文字設定を使用 */
.ods-page,
.ods-page * {
    font-family: var(--description-font) !important;
    font-size: var(--description-size) !important;
}

/* アプリ名だけは他の見出しより大きくする */
.app-title,
.startup-title {
    font-size: calc(var(--heading-size) + 15px) !important;
}

/* 月別画面の年表示 */
.detail-year {
    font-size: var(--month-size) !important;
}

.setting-section p,
.setting-section .setting-row > div > div:nth-child(2),
.setting-section > div[style*="margin-top:10px"] {
    font-family: var(--setting-font, inherit) !important;
    font-size: var(--setting-size) !important;
}

/* ライトモード:一覧の見出し背景を不透明にする */
body:not(.dark) .list-table th {
    background-color: #e9f0e9;
    color: #222;
    opacity: 1;
    z-index: 5;
}

/* ライトモード:左端の年月見出しを最前面に */
body:not(.dark) .list-table th:first-child {
    background-color: #e9f0e9;
    color: #222;
    opacity: 1;
    z-index: 6;
}

</style>
</head>

<body>

<!-- =====================================================
     起動画面
===================================================== -->

<div id="startupScreen">

    <div class="startup-card">

        <div class="startup-title">
            支出管理
        </div>

        <div class="startup-description">
            指定したフォルダに記録を保存します。<br>

        </div>

        <div
            class="folder-status"
            id="startupFolderStatus"
        >
            保存フォルダが指定されていません。
        </div>

<button
    class="primary-btn"
    id="restoreFolderBtn"
>
    前回フォルダを許可
</button>

<button
    class="primary-btn"
    id="selectFolderBtn"
    style="margin-top:10px;"
>
    新しくフォルダを指定
</button>

        <div
            style="
                margin-top:18px;
                font-size:13px;
                color:var(--sub-text);
            "
        >
            
        </div>

    </div>

</div>


<!-- =====================================================
     アプリ本体
===================================================== -->

<div id="app" class="hidden">

    <header class="topbar">

        <div
            class="app-title"
            id="appTitle"
        >
            支出管理
        </div>

        <button
            class="nav-btn"
            data-page="years"
        >
            年別
        </button>

        <button
            class="nav-btn"
            data-page="add"
        >
            追加
        </button>

<button
    class="nav-btn"
    data-page="list"
>
    一覧
</button>

        <button
            class="nav-btn"
            data-page="settings"
        >
            設定
        </button>

        <button
            class="nav-btn"
            data-page="background"
        >
            背景画像
        </button>

        <button
            class="nav-btn"
            data-page="description"
        >
            アプリ説明
        </button>

        <div class="nav-spacer"></div>

        <div
            class="folder-indicator"
            id="folderIndicator"
        ></div>

        <button
            class="nav-btn"
            id="resetBtn"
            style="color:var(--danger);"
        >
            記録初期化
        </button>

    </header>

    <main
        class="main"
        id="main"
    ></main>

</div>


<!-- =====================================================
     モーダル
===================================================== -->

<div
    id="modal"
    class="modal hidden"
>

    <div
        class="modal-card"
        id="modalCard"
    ></div>

</div>


<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>

<script>
"use strict";

/* =========================================================
   定数
========================================================= */

const DATA_FILE =
"expenditure-data.json";
const SETTINGS_FILE =
"settings.json";
const DRAFT_FILE =
"expenditure-draft.json";

let directoryHandle = null;

let data = {
    months: {},
    bonuses: [],
    yearMemos: {}
};

let settings = {
darkMode: false,
mainColor: "#4f6f52",
backgroundColor: "#f4f6f4",
backgroundOpacity: .85,
backgroundImageName: "",
appTitle: "支出管理",
fontSizes: {
heading: 20,
year: 15,
month: 15,
add: 15,
setting: 15,
description: 16
},
fontFamilies: {
heading: "yu-gothic",
year: "yu-gothic",
month: "yu-gothic",
add: "yu-gothic",
setting: "yu-gothic",
description: "yu-gothic"
}
};

let currentPage = "years";
let editingMonthKey = null;
let averageWindow = null;


/* =========================================================
   初期化
========================================================= */

document.addEventListener(
    "DOMContentLoaded",
    () => {

        document
    .getElementById("restoreFolderBtn")
    .addEventListener(
        "click",
        () => {
            restorePreviousFolder(true);
        }
    );

document
    .getElementById("selectFolderBtn")
    .addEventListener(
        "click",
        selectFolder
    );

        document
            .querySelectorAll(
                ".nav-btn[data-page]"
            )
            .forEach(btn => {

                btn.addEventListener(
                    "click",
                    () => {

                        showPage(
                            btn.dataset.page
                        );

                    }
                );

            });

        document
            .getElementById("resetBtn")
            .addEventListener(
                "click",
                resetRecords
            );

document.addEventListener(
  "click",
  e => {
    if (
      averageWindow &&
      !averageWindow.closed
    ) {
      if (
        !e.target.closest(
          'button[onclick^="showYearAverage"]'
        )
      ) {
        averageWindow.close();
        averageWindow = null;
      }
    }
  }
);

        document
            .getElementById("modal")
            .addEventListener(
                "click",
                e => {

                    if (
                        e.target.id === "modal"
                    ) {
                        closeModal();
                    }

                }
            );

        if (
!("showDirectoryPicker" in window)
) {
document
.getElementById(
"startupFolderStatus"
)
.innerHTML =
'<span class="warning">' +
'このブラウザはフォルダ保存機能に対応していません。' +
'ChromeまたはEdge等をご利用ください。' +
'</span>';
} else {
restorePreviousFolder();
}
}
);

/* =========================================================
フォルダ記憶
========================================================= */
const FOLDER_DB_NAME = "expenditure-folder-db";
const FOLDER_STORE_NAME = "handles";
const FOLDER_HANDLE_KEY = "directory";

function openFolderDB() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(
            FOLDER_DB_NAME,
            1
        );

        request.onupgradeneeded = () => {
            const db = request.result;

            if (
                !db.objectStoreNames.contains(
                    FOLDER_STORE_NAME
                )
            ) {
                db.createObjectStore(
                    FOLDER_STORE_NAME
                );
            }
        };

        request.onsuccess = () => {
            resolve(request.result);
        };

        request.onerror = () => {
            reject(request.error);
        };
    });
}

async function deleteSavedDirectoryHandle() {
    const db =
        await openFolderDB();

    return new Promise(
        (resolve, reject) => {
            const transaction =
                db.transaction(
                    FOLDER_STORE_NAME,
                    "readwrite"
                );

            const store =
                transaction.objectStore(
                    FOLDER_STORE_NAME
                );

            const request =
                store.delete(
                    FOLDER_HANDLE_KEY
                );

            request.onsuccess = () => {
                db.close();
                resolve();
            };

            request.onerror = () => {
                db.close();
                reject(
                    request.error
                );
            };
        }
    );
}

async function saveDirectoryHandle(
    handle
) {
    const db =
        await openFolderDB();

    return new Promise(
        (resolve, reject) => {
            const transaction =
                db.transaction(
                    FOLDER_STORE_NAME,
                    "readwrite"
                );

            transaction.objectStore(
                FOLDER_STORE_NAME
            ).put(
                handle,
                FOLDER_HANDLE_KEY
            );

            transaction.oncomplete = () => {
                db.close();
                resolve();
            };

            transaction.onerror = () => {
                db.close();
                reject(
                    transaction.error
                );
            };
        }
    );
}

async function loadDirectoryHandle() {
    const db =
        await openFolderDB();

    return new Promise(
        (resolve, reject) => {
            const transaction =
                db.transaction(
                    FOLDER_STORE_NAME,
                    "readonly"
                );

            const request =
                transaction.objectStore(
                    FOLDER_STORE_NAME
                ).get(
                    FOLDER_HANDLE_KEY
                );

            request.onsuccess = () => {
                db.close();
                resolve(
                    request.result || null
                );
            };

            request.onerror = () => {
                db.close();
                reject(
                    request.error
                );
            };
        }
    );
}

async function restorePreviousFolder(
    requestAccess = false
) {
    try {

        const handle =
            await loadDirectoryHandle();

        /*
         * 前回フォルダ自体が記憶されていない
         */
        if (!handle) {

            directoryHandle = null;

            document
                .getElementById(
                    "startupFolderStatus"
                )
                .innerHTML =
                "前回の保存フォルダはありません。<br>" +
                "新しくフォルダを指定してください。";

            return false;
        }

        directoryHandle =
            handle;

        /*
         * 現在の権限を確認
         */
        let permission =
            await handle.queryPermission({
                mode: "readwrite"
            });

        /*
         * 起動時の自動確認では、
         * ここで止める。
         *
         * 権限要求はユーザーが
         * 「前回フォルダを許可」を
         * 押したときだけ行う。
         */
        if (
            permission !== "granted" &&
            !requestAccess
        ) {

            document
                .getElementById(
                    "startupFolderStatus"
                )
                .innerHTML =
                "前回の保存フォルダが見つかりました。<br>" +
                "アクセス許可が必要です。";

            return false;
        }

        /*
         * 「前回フォルダを許可」が
         * 押された場合だけ権限を要求する。
         */
        if (
            permission !== "granted" &&
            requestAccess
        ) {

            permission =
                await handle.requestPermission({
                    mode: "readwrite"
                });

            if (
                permission !== "granted"
            ) {

                directoryHandle = null;

                document
                    .getElementById(
                        "startupFolderStatus"
                    )
                    .innerHTML =
                    '<span class="warning">' +
                    "前回の保存フォルダへのアクセスが許可されませんでした。" +
                    "</span>";

                return false;
            }
        }

        /*
         * 保存フォルダとして使えるか確認。
         *
         * data / settings / draft の
         * いずれかが存在すれば、
         * 前回使用していたフォルダと判断する。
         */
        let hasSavedFile = false;

        for (
            const fileName of [
                DATA_FILE,
                SETTINGS_FILE,
                DRAFT_FILE
            ]
        ) {

            try {

                await directoryHandle.getFileHandle(
                    fileName,
                    {
                        create: false
                    }
                );

                hasSavedFile = true;
                break;

            } catch (error) {

                if (
                    error.name !==
                    "NotFoundError"
                ) {
                    throw error;
                }
            }
        }

        /*
         * 前回フォルダに
         * 保存ファイルが存在しない。
         */
        if (!hasSavedFile) {

            directoryHandle = null;

            await deleteSavedDirectoryHandle();

            document
                .getElementById(
                    "startupFolderStatus"
                )
                .innerHTML =
                '<span class="warning">' +
                "前回の保存フォルダを確認できませんでした。<br>" +
                "保存データが存在しないため、" +
                "新しい保存フォルダを指定してください。" +
                "</span>";

            return false;
        }

        /*
         * 前回フォルダのデータを読み込む。
         */
        await loadData();

        /*
         * アプリ起動
         */
        document
            .getElementById(
                "startupScreen"
            )
            .classList
            .add("hidden");

        document
            .getElementById("app")
            .classList
            .remove("hidden");

        document
            .getElementById(
                "folderIndicator"
            )
            .textContent =
            "保存先: " +
            directoryHandle.name;

        applySettings();
        const lastPage = settings.lastPage || "years";
await showPage(lastPage);

        return true;
} catch (error) {

    console.error(
        "前回のフォルダを復元できませんでした。",
        error
    );

    /*
     * JSONの読み込み・解析エラーの場合。
     *
     * フォルダ自体にはアクセスできているため、
     * 新しいフォルダを要求しない。
     */
    if (
        error instanceof SyntaxError
    ) {

        document
            .getElementById(
                "startupFolderStatus"
            )
            .innerHTML =
            '<span class="warning">' +
            "保存データまたは設定ファイルのJSONが壊れています。<br>" +
            "ファイルの内容を確認してください。" +
            "</span>";

        return false;
    }

    /*
     * それ以外は、
     * フォルダへのアクセス自体に
     * 問題がある可能性がある。
     */
    directoryHandle = null;

    document
        .getElementById(
            "startupFolderStatus"
        )
        .innerHTML =
        '<span class="warning">' +
        "前回の保存フォルダを利用できません。<br>" +
        "新しい保存フォルダを指定してください。" +
        "</span>";

    return false;
}

}

/* =========================================================
   フォルダ
========================================================= */

async function selectFolder() {
    try {

        /*
         * ユーザーが明示的に
         * 「新しくフォルダを指定」を押した場合。
         *
         * 前回フォルダの確認は行わない。
         */
        directoryHandle =
            await window.showDirectoryPicker({
                mode: "readwrite"
            });

        /*
         * ユーザーが選択したフォルダを
         * IndexedDBへ記憶する。
         */
        await saveDirectoryHandle(
            directoryHandle
        );

        /*
         * 新しいフォルダなので、
         * 空フォルダでも正常に開始する。
         */
        await loadData();

        /*
         * 起動画面を閉じる。
         */
        document
            .getElementById(
                "startupScreen"
            )
            .classList
            .add("hidden");

        document
            .getElementById("app")
            .classList
            .remove("hidden");

        document
            .getElementById(
                "folderIndicator"
            )
            .textContent =
            "保存先: " +
            directoryHandle.name;

        applySettings();
        showPage("years");

    } catch (error) {

        console.error(
            "新しいフォルダの指定に失敗しました。",
            error
        );

        /*
         * ユーザーが選択をキャンセルした場合。
         */
        if (
            error.name ===
            "AbortError"
        ) {
            return;
        }

        directoryHandle = null;

        document
            .getElementById(
                "startupFolderStatus"
            )
            .innerHTML =
            '<span class="warning">' +
            "フォルダへのアクセスに失敗しました。" +
            "</span>";
    }
}

/* =========================================================
   ファイル読み書き
========================================================= */

async function getFileHandle(
    name,
    create = true
) {

    return await directoryHandle
        .getFileHandle(
            name,
            { create }
        );

}


async function readJsonFile(
    name,
    defaultValue
) {

    try {

        const fileHandle =
            await getFileHandle(
                name,
                false
            );

        const file =
            await fileHandle.getFile();

        const text =
            await file.text();

        if (!text.trim()) {

            return defaultValue;

        }

        return JSON.parse(text);

    } catch (error) {

        if (
            error.name ===
            "NotFoundError"
        ) {

            return defaultValue;

        }

        throw error;

    }

}

async function writeJsonFile(
    name,
    value
) {

    const fileHandle =
        await getFileHandle(
            name,
            true
        );

    const writable =
        await fileHandle.createWritable();

    await writable.write(
        JSON.stringify(
            value,
            null,
            2
        )
    );

    await writable.close();

}

async function deleteJsonFile(name) {
    if (!directoryHandle) {
        return;
    }

    try {
        await directoryHandle.removeEntry(name);

        console.log(
            name + " を削除しました。"
        );

    } catch (error) {

        /*
         * すでに存在しない場合は正常。
         */
        if (
            error.name ===
            "NotFoundError"
        ) {
            return;
        }

        throw error;
    }
}

async function loadData() {

    data =
        await readJsonFile(
            DATA_FILE,
            {
                months: {},
                yearMemos: {}
            }
        );

    settings =
        await readJsonFile(
            SETTINGS_FILE,
            settings
        );

    if (!data.months) {
        data.months = {};
    }

    if (!data.bonuses) {
        data.bonuses = [];
    }

    if (!data.yearMemos) {
        data.yearMemos = {};
    }

    if (!settings.fontSizes) {
        settings.fontSizes = {};
    }

    if (settings.fontSizes.heading === undefined) {
        settings.fontSizes.heading = 20;
    }

    if (settings.fontSizes.year === undefined) {
        settings.fontSizes.year = 15;
    }

    if (settings.fontSizes.month === undefined) {
        settings.fontSizes.month = 15;
    }

    if (settings.fontSizes.add === undefined) {
        settings.fontSizes.add = 15;
    }

    if (settings.fontSizes.setting === undefined) {
        settings.fontSizes.setting = 15;
    }

    if (settings.fontSizes.description === undefined) {
        settings.fontSizes.description = 16;
    }

    if (!settings.fontFamilies) {
        settings.fontFamilies = {};
    }

    [
        "heading",
        "year",
        "month",
        "add",
        "setting",
        "description"
    ].forEach(type => {
        if (!settings.fontFamilies[type]) {
            settings.fontFamilies[type] = "yu-gothic";
        }
    });
}

async function saveData() {

    await writeJsonFile(
        DATA_FILE,
        data
    );

}


async function saveDraft() {
    if (!directoryHandle) {
        return;
    }

    const getNumber = id =>
        Number(
            document
                .getElementById(id)
                .value || 0
        );

    const draft = {
        editingMonthKey:
            editingMonthKey,

        month: {
            yearMonth:
                document
                    .getElementById(
                        "f-yearMonth"
                    )
                    .value,

            income:
                getNumber("f-income"),

            overtimeHours:
                getNumber(
                    "f-overtimeHours"
                ),

            overtimePay:
                getNumber(
                    "f-overtimePay"
                ),

            grossPay:
                getNumber("f-grossPay"),

            expense:
                getNumber("f-expense"),

            financialAsset1:
                getNumber(
                    "f-financialAsset1"
                ),

            financialAsset2:
                getNumber(
                    "f-financialAsset2"
                ),

            temporaryIncome:
                collectLineEditor(
                    "temporaryIncome"
                ),

            expenses:
                collectLineEditor(
                    "expenses"
                ),

            asset:
                getNumber("f-asset"),

            loan:
                getNumber("f-loan"),

            saving:
                getNumber("f-saving"),

            electricity: {
                kwh:
                    getNumber(
                        "f-electricity-kwh"
                    ),

                amount:
                    getNumber(
                        "f-electricity-amount"
                    )
            },

            gas: {
                volume:
                    getNumber(
                        "f-gas-volume"
                    ),

                amount:
                    getNumber(
                        "f-gas-amount"
                    )
            },

            water: {
                volume:
                    getNumber(
                        "f-water-volume"
                    ),

                amount:
                    getNumber(
                        "f-water-amount"
                    )
            },

            
            memo:
                document
                    .getElementById(
                        "f-memo"
                    )
                    .value
        }
    };

    /*
     * 何も入力されていないか確認。
     */

    const month =
        draft.month;

    const hasTemporaryIncome =
        month.temporaryIncome.some(
            item =>
                String(
                    item.memo || ""
                ).trim() !== "" ||
                Number(
                    item.amount || 0
                ) !== 0
        );

    const hasExpenses =
        month.expenses.some(
            item =>
                String(
                    item.memo || ""
                ).trim() !== "" ||
                Number(
                    item.amount || 0
                ) !== 0
        );

    const hasInput =
        String(
            month.yearMonth || ""
        ).trim() !== "" ||

        month.income !== 0 ||

        month.overtimeHours !== 0 ||

        month.overtimePay !== 0 ||

        month.grossPay !== 0 ||

        month.expense !== 0 ||

        month.financialAsset1 !== 0 ||

        month.financialAsset2 !== 0 ||

        hasTemporaryIncome ||

        hasExpenses ||

        month.asset !== 0 ||

        month.loan !== 0 ||

        month.saving !== 0 ||

        month.electricity.kwh !== 0 ||

        month.electricity.amount !== 0 ||

        month.gas.volume !== 0 ||

        month.gas.amount !== 0 ||

        month.water.volume !== 0 ||

        month.water.amount !== 0 ||

        String(
            month.memo || ""
        ).trim() !== "";

    /*
     * 完全に空ならdraftを作らない。
     *
     * すでに古い空draftがあれば削除する。
     */
    if (!hasInput) {
        await deleteDraft();
        return;
    }

    /*
     * 何か入力されている場合だけ
     * draftとして保存する。
     */
    try {
        await writeJsonFile(
            DRAFT_FILE,
            draft
        );

        console.log(
            "編集中データを一時保存しました。"
        );

    } catch (error) {
        console.error(
            "draftの保存に失敗しました。",
            error
        );

        alert(
            "編集中データの一時保存に失敗しました。"
        );
    }
}

async function loadDraft() {
    if (!directoryHandle) {
        return null;
    }

    try {
        const fileHandle =
            await directoryHandle.getFileHandle(
                DRAFT_FILE
            );

        const file =
            await fileHandle.getFile();

        const text =
            await file.text();

        /*
         * 空ファイルはdraftなしとして扱う。
         */
        if (!text.trim()) {
            return null;
        }

        const draft =
            JSON.parse(text);

        /*
         * draftの基本構造を確認。
         */
        if (
            !draft ||
            typeof draft !== "object" ||
            !draft.month ||
            typeof draft.month !== "object"
        ) {
            throw new Error(
                "draftの形式が正しくありません。"
            );
        }

        /*
         * month.yearMonth が存在する場合は、
         * 年月として正しい形式か確認。
         *
         * 空の場合は「新規入力のdraft」として許可する。
         */
        const yearMonth =
            draft.month.yearMonth;

        if (
            yearMonth !== undefined &&
            yearMonth !== "" &&
            !/^\d{4}-\d{2}$/.test(
                String(yearMonth)
            )
        ) {
            throw new Error(
                "draftの年月データが正しくありません。"
            );
        }

        /*
         * 明細が存在しない場合は
         * 復元時に空の1行を作れるため許可する。
         */
        if (
            draft.month.temporaryIncome !== undefined &&
            !Array.isArray(
                draft.month.temporaryIncome
            )
        ) {
            throw new Error(
                "draftの臨時収入データが正しくありません。"
            );
        }

        if (
            draft.month.expenses !== undefined &&
            !Array.isArray(
                draft.month.expenses
            )
        ) {
            throw new Error(
                "draftの出費データが正しくありません。"
            );
        }

        return draft;

    } catch (error) {

        /*
         * draftが存在しない場合は正常。
         */
        if (
            error.name ===
            "NotFoundError"
        ) {
            return null;
        }

        console.error(
            "draftの読み込みに失敗しました。",
            error
        );

        /*
         * 壊れたdraftを
         * 勝手に削除しない。
         *
         * ユーザーの入力データを
         * 勝手に消さないため。
         */
        alert(
            "一時保存データを読み込めませんでした。\n\n" +
            "draftファイルが壊れているか、" +
            "形式が正しくありません。\n\n" +
            "一時保存データは削除していません。"
        );

        return null;
    }
}

function restoreDraft(draft) {
    if (!draft || !draft.month) {
        return;
    }

    const month = draft.month;

    document.getElementById(
        "f-yearMonth"
    ).value =
        month.yearMonth || "";

    document.getElementById(
        "f-income"
    ).value =
        month.income ?? 0;

    document.getElementById(
        "f-overtimeHours"
    ).value =
        month.overtimeHours ?? 0;

    document.getElementById(
        "f-overtimePay"
    ).value =
        month.overtimePay ?? 0;

    document.getElementById(
        "f-grossPay"
    ).value =
        month.grossPay ?? 0;

    document.getElementById(
        "f-expense"
    ).value =
        month.expense ?? 0;

    document.getElementById(
        "f-financialAsset1"
    ).value =
        month.financialAsset1 ?? 0;

    document.getElementById(
        "f-financialAsset2"
    ).value =
        month.financialAsset2 ?? 0;

    document.getElementById(
        "f-asset"
    ).value =
        month.asset ?? 0;

    document.getElementById(
        "f-loan"
    ).value =
        month.loan ?? 0;

    document.getElementById(
        "f-saving"
    ).value =
        month.saving ?? 0;

    document.getElementById(
        "f-electricity-kwh"
    ).value =
        month.electricity?.kwh ?? 0;

    document.getElementById(
        "f-electricity-amount"
    ).value =
        month.electricity?.amount ?? 0;

    document.getElementById(
        "f-gas-volume"
    ).value =
        month.gas?.volume ?? 0;

    document.getElementById(
        "f-gas-amount"
    ).value =
        month.gas?.amount ?? 0;

    document.getElementById(
        "f-water-volume"
    ).value =
        month.water?.volume ?? 0;

    document.getElementById(
        "f-water-amount"
    ).value =
        month.water?.amount ?? 0;

    document.getElementById(
        "f-memo"
    ).value =
        month.memo || "";

    /*
     * 臨時収入を復元
     */

    renderLineEditor(
    "temporaryIncomeEditor",
    month.temporaryIncome &&
    month.temporaryIncome.length
        ? month.temporaryIncome
        : [
            {
                memo: "",
                amount: 0
            }
        ],
    "temporaryIncome"
);

    /*
     * 出費を復元
     */
    renderLineEditor(
    "expenseEditor",
    month.expenses &&
    month.expenses.length
        ? month.expenses
        : [
            {
                memo: "",
                amount: 0
            }
        ],
    "expenses"
);

    editingMonthKey =
        draft.editingMonthKey || null;

    console.log(
        "draftを復元しました。"
    );
}

async function deleteDraft() {
    if (!directoryHandle) {
        return;
    }

    try {
        await deleteJsonFile(
            DRAFT_FILE
        );

        console.log(
            "draftを削除しました。"
        );

    } catch (error) {
        console.error(
            "draftの削除に失敗しました。",
            error
        );
    }
}

async function saveSettings() {

    await writeJsonFile(
        SETTINGS_FILE,
        settings
    );

}


/* =========================================================
   ページ切り替え
========================================================= */

async function showPage(page) {

    /*
     * 追加・編集画面から別ページへ移動する場合、
     * 正式保存とは別に編集中の内容を一時保存する。
     */
    if (
    currentPage === "add" &&
    page !== "add" &&
    document.getElementById("f-yearMonth")
) {
    await saveDraft();
}

    currentPage = page;

if (page !== null) {
    settings.lastPage = page;

    if (directoryHandle) {
        try {
            await saveSettings();
        } catch (error) {
            console.error(
                "ページ切り替え時の設定保存に失敗しました。",
                error
            );
        }
    }
}

    /*
     * 上部バーの選択状態を変更
     */
    document
        .querySelectorAll(
            ".nav-btn[data-page]"
        )
        .forEach(btn => {
            btn.classList.toggle(
                "active",
                btn.dataset.page === page
            );
        });

    /*
     * 先に各ページを表示する。
     */
    if (page === "years") {
        renderYears();
    }

if (page === "list") {
    renderListPage();
}

    if (page === "settings") {
        renderSettings();
    }

    if (page === "background") {
        renderBackgroundPage();
    }

    if (page === "description") {
    if (typeof renderDescription === "function") {
        renderDescription();
    } else {
        console.error(
            "renderDescription() が定義されていません。"
        );

        document.getElementById("main").innerHTML = `
            <h1 class="page-title">アプリ説明</h1>
            <p class="page-description">
                説明画面の描画処理が見つかりません。
                年別画面へ戻って、ほかの機能をご利用ください。
            </p>
            <button
                class="primary-btn"
                onclick="showPage('years')"
            >
                年別画面へ戻る
            </button>
        `;
    }
}

if (page === "ods") {
    renderOdsPage();
}

    /*
     * 追加画面は、
     * まず入力欄を作ってからdraftを確認する。
     */
    if (page === "add") {


        renderAddPage();

        const draft =
            await loadDraft();

        if (draft) {

            const restore =
                confirm(
                    "編集中のデータがあります。\n\n" +
                    "前回の入力内容を復元しますか?"
                );

            if (restore) {

                restoreDraft(draft);

            } else {

                await deleteDraft();

            }
        }
    }
}

    
/* =========================================================
   年取得
========================================================= */

function getYears() {

    const years = [
        ...Object.keys(data.months)
            .map(
                key => key.substring(0, 4)
            ),

        ...(data.bonuses || [])
            .map(
                bonus =>
                    bonus.yearMonth.substring(0, 4)
            )
    ];

    return years
        .filter(
            (year, index, arr) =>
                arr.indexOf(year) === index
        )
        .sort(
            (a, b) =>
                Number(b) - Number(a)
        );
}

function getYearMonths(year) {

    return Object.entries(
        data.months
    )

    .filter(
        ([key]) =>
            key.startsWith(
                year + "-"
            )
    )

    .sort(
        ([a],[b]) =>
            a.localeCompare(b)
    )

    .map(
        ([,value]) => value
    );

}


/* =========================================================
   年集計
========================================================= */

function sumNumbers(values) {

    return values.reduce(
        (sum,value) => {

            return sum +
                Number(value || 0);

        },
        0
    );

}


function calculateYear(year) {

    const months =
        getYearMonths(year);

    const bonusList =
        (data.bonuses || [])
            .filter(
                bonus =>
                    bonus.yearMonth.startsWith(
                        year + "-"
                    )
            );

    const bonusIncomeTotal =
        sumNumbers(
            bonusList.map(
                bonus => bonus.income
            )
        );

    const bonusGrossTotal =
        sumNumbers(
            bonusList.map(
                bonus => bonus.grossPay
            )
        );

    const income =
        sumNumbers(
            months.map(
                m => m.income
            )
        ) +
        bonusIncomeTotal;

    const gross =
        sumNumbers(
            months.map(
                m => m.grossPay
            )
        ) +
        bonusGrossTotal;

    const expense =
        sumNumbers(
            months.map(
                m => m.expense
            )
        );

    const loan =
        sumNumbers(
            months.map(
                m => m.loan
            )
        );

    const saving =
        sumNumbers(
            months.map(
                m => m.saving
            )
        );

    const balance =
        income -
        expense -
        loan -
        saving;

    const asset =
        months.length
            ? Number(
                months[
                    months.length - 1
                ].asset || 0
            )
            : 0;

    return {
        income,
        gross,
        expense,
        balance,
        asset
    };
}

function calculateCumulativeFinancialAssets(year) {
    const targetYear = Number(year);

    const months = Object.values(
        data.months || {}
    ).filter(month =>
        Number(
            String(month.yearMonth || "").substring(0, 4)
        ) <= targetYear
    );

    const bonuses = (
        data.bonuses || []
    ).filter(bonus =>
        Number(
            String(bonus.yearMonth || "").substring(0, 4)
        ) <= targetYear
    );

    const financialAsset1 =
        sumNumbers(
            months.map(month => month.financialAsset1)
        ) +
        sumNumbers(
            bonuses.map(bonus => bonus.financialAsset1)
        );

    const financialAsset2 =
        sumNumbers(
            months.map(month => month.financialAsset2)
        ) +
        sumNumbers(
            bonuses.map(bonus => bonus.financialAsset2)
        );

    return {
        financialAsset1,
        financialAsset2,
        total: financialAsset1 + financialAsset2
    };
}

function showCumulativeFinancialAssets(year) {
    const result =
        calculateCumulativeFinancialAssets(year);

    alert(
        `${year}年までの累計金融資産\n\n` +
        `金融資産1:${money(result.financialAsset1)}\n` +
        `金融資産2:${money(result.financialAsset2)}\n` +
        `合計:${money(result.total)}`
    );
}

/* =========================================================
   前年比
   %+金額
========================================================= */

function getYearChange(
    current,
    previous
) {

    if (
        previous === null ||
        previous === undefined
    ) {

        return {
            text: "-",
            negative: false
        };

    }

    const difference =
        current - previous;

    if (previous === 0) {

        if (current === 0) {

            return {
                text: "0.0% / 0",
                negative: false
            };

        }

        return {
            text: "- / " +
                formatSignedMoney(
                    difference
                ),
            negative:
                difference < 0
        };

    }

    const percent =
        (
            difference /
            Math.abs(previous)
        ) * 100;

    const percentText =
        (percent > 0 ? "+" : "") +
        percent.toFixed(1) +
        "%";

    const moneyText =
        formatSignedMoney(
            difference
        );

    return {
        text:
            percentText +
            " / " +
            moneyText,

        negative:
            difference < 0
    };

}


function formatSignedMoney(
    value
) {

    const number =
        Number(value || 0);

    if (number > 0) {

        return "+" +
            money(number);

    }

    if (number < 0) {

        return "-" +
            money(
                Math.abs(number)
            );

    }

    return "0";

}


function money(value) {
  return Math.floor(
    Number(value || 0)
  ).toLocaleString(
    "ja-JP"
  );
}

/* =========================================================
   年別画面
========================================================= */

function renderYears() {

    let html = `

        <p class="page-description">
            年ごとの所得・総支給・支出・収支・資産を確認できます。
            年をクリックすると、その年の詳細を表示します。
        </p>

    `;

    const years =
        getYears();

    if (!years.length) {

        html += `

            <div class="form-card">

                まだ記録がありません。<br>

                「追加」から月の記録を登録してください。

            </div>

        `;

        document
            .getElementById("main")
            .innerHTML = html;

        return;
    }

    html += `

        <div class="year-table-wrapper">

            <table class="year-table">

                <thead>

                    <tr>
                        <th>年</th>
                        <th>所得</th>
                        <th>総支給</th>
                        <th>支出</th>
                        <th>収支</th>
                        <th>資産</th>
<th>累計金融資産</th>
                        <th>メモ編集</th>
                    </tr>

                </thead>

                <tbody>

    `;

    years.forEach(year => {

        const current =
            calculateYear(year);

        const previousYear =
            years.find(
                y =>
                    Number(y) ===
                    Number(year) - 1
            );

        const previous =
            previousYear
                ? calculateYear(
                    previousYear
                )
                : null;

        const incomeChange =
            getYearChange(
                current.income,
                previous
                    ? previous.income
                    : null
            );

        const grossChange =
            getYearChange(
                current.gross,
                previous
                    ? previous.gross
                    : null
            );

        const expenseChange =
            getYearChange(
                current.expense,
                previous
                    ? previous.expense
                    : null
            );

        const assetChange =
            getYearChange(
                current.asset,
                previous
                    ? previous.asset
                    : null
            );

        html += `
<tr>
<td>
<div style="
    display:flex;
    align-items:center;
    gap:10px;
">
<span
    class="year-link"
    onclick="showYearDetail('${year}')"
>
${year}
</span>

<button
    class="secondary-btn"
    style="
        padding:6px 12px;
        font-size:13px;
    "
    onclick="showYearAverage('${year}')"
>
平均
</button>
</div>
</td>

                                    <td>
    <div>${money(current.income)}</div>
    <div class="year-change ${incomeChange.negative ? "negative" : ""}">
        <div>前年比 【${incomeChange.text.split(" / ")[0]}】</div>
        <div>【${incomeChange.text.split(" / ")[1] || ""}】</div>
    </div>
</td>

<td>
    <div>${money(current.gross)}</div>
    <div class="year-change ${grossChange.negative ? "negative" : ""}">
        <div>前年比 【${grossChange.text.split(" / ")[0]}】</div>
        <div>【${grossChange.text.split(" / ")[1] || ""}】</div>
    </div>
</td>

<td>
    <div>${money(current.expense)}</div>
    <div class="year-change ${expenseChange.negative ? "negative" : ""}">
        <div>前年比 【${expenseChange.text.split(" / ")[0]}】</div>
        <div>【${expenseChange.text.split(" / ")[1] || ""}】</div>
    </div>
</td>

<td>
    ${money(current.balance)}
</td>

<td>
    <div>${money(current.asset)}</div>
    <div class="year-change ${assetChange.negative ? "negative" : ""}">
        <div>前年比 【${assetChange.text.split(" / ")[0]}】</div>
        <div>【${assetChange.text.split(" / ")[1] || ""}】</div>
    </div>
</td>
                

<td>
    <span
        class="year-link"
        onclick="showCumulativeFinancialAssets('${year}')"
    >
        ${money(
            calculateCumulativeFinancialAssets(year).total
        )}
    </span>
</td>

                <td>

    <span class="memo-status">
        <input
            type="checkbox"
            ${
              String(
    (data.yearMemos || {})[year] || ""
).trim() !== ""

                    ? "checked"
                    : ""
            }
            tabindex="-1"
        >
    </span>

    <button
        class="memo-edit-btn"
        onclick="openYearMemo('${year}')"
    >
        編集
    </button>

</td>

            </tr>

        `;

    });

    html += `

                </tbody>

            </table>

        </div>

    `;

    document
        .getElementById("main")
        .innerHTML = html;

}

/* =========================================================
   年平均
========================================================= */

function showYearAverage(year) {

    /*
     * 通常月のみを平均計算に使用
     * 賞与は「賞与込み」の項目だけに加算する。
     */
    const months =
        getYearMonths(year);

    const monthCount =
        months.length;

console.table(
    months.map(month => ({
        yearMonth: month.yearMonth,
        income: month.income,
        grossPay: month.grossPay
    }))
);

console.log(
    "通常月数:",
    monthCount
);

    const bonusList =
        (data.bonuses || [])
            .filter(
                bonus =>
                    bonus.yearMonth.startsWith(
                        year + "-"
                    )
            );

    /*
     * 通常所得
     */
    const normalIncome =
        monthCount
            ? sumNumbers(
                months.map(
                    month =>
                        month.income
                )
            ) / monthCount
            : 0;

    /*
     * 賞与所得
     */
    const bonusIncome =
        sumNumbers(
            bonusList.map(
                bonus =>
                    bonus.income
            )
        );

    /*
     * 賞与込み所得
     */
    const incomeWithBonus =
        monthCount
            ? (
                sumNumbers(
                    months.map(
                        month =>
                            month.income
                    )
                ) +
                bonusIncome
            ) / monthCount
            : 0;

    /*
     * 通常総支給
     */
    const normalGross =
        monthCount
            ? sumNumbers(
                months.map(
                    month =>
                        month.grossPay
                )
            ) / monthCount
            : 0;

    /*
     * 賞与総支給
     */
    const bonusGross =
        sumNumbers(
            bonusList.map(
                bonus =>
                    bonus.grossPay
            )
        );

    /*
     * 賞与込み総支給
     */
    const grossWithBonus =
        monthCount
            ? (
                sumNumbers(
                    months.map(
                        month =>
                            month.grossPay
                    )
                ) +
                bonusGross
            ) / monthCount
            : 0;

    /*
     * 支出
     */
    const averageExpense =
        monthCount
            ? sumNumbers(
                months.map(
                    month =>
                        month.expense
                )
            ) / monthCount
            : 0;

    /*
     * 収支
     *
     * 通常月の
     * 所得 - 支出 - ローン - 積立
     *
     * を各月で計算して平均する。
     * 賞与は収支平均には含めない。
     */
    const averageBalance =
        monthCount
            ? sumNumbers(
                months.map(
                    month =>
                        Number(
                            month.income || 0
                        ) -
                        Number(
                            month.expense || 0
                        ) -
                        Number(
                            month.loan || 0
                        ) -
                        Number(
                            month.saving || 0
                        )
                )
            ) / monthCount
            : 0;

    /*
     * 電気
     */
    const averageElectricity =
        monthCount
            ? sumNumbers(
                months.map(
                    month =>
                        month.electricity?.amount || 0
                )
            ) / monthCount
            : 0;

    /*
     * ガス
     */
    const averageGas =
        monthCount
            ? sumNumbers(
                months.map(
                    month =>
                        month.gas?.amount || 0
                )
            ) / monthCount
            : 0;

    /*
     * 水道
     */
    const averageWater =
        monthCount
            ? sumNumbers(
                months.map(
                    month =>
                        month.water?.amount || 0
                )
            ) / monthCount
            : 0;

    /*
     * 別ウィンドウ
     */
    if (
  averageWindow &&
  !averageWindow.closed
) {
  averageWindow.focus();
} else {
  averageWindow =
    window.open(
      "",
      "yearAverageWindow",
      "width=500,height=650,resizable=yes,scrollbars=yes"
    );
}

if (!averageWindow) {
  alert(
    "平均画面を開けませんでした。\n" +
    "ブラウザのポップアップブロックを確認してください。"
  );
  return;
}

    if (!averageWindow) {

        alert(
            "平均画面を開けませんでした。\n" +
            "ブラウザのポップアップブロックを確認してください。"
        );

        return;
    }

    averageWindow.document.write(`
<!DOCTYPE html>
<html lang="ja">
<head>

<meta charset="UTF-8">

<title>${year}年 平均</title>

<style>

* {
    box-sizing:border-box;
}

body {
    margin:0;
    padding:30px;
    font-family:
        -apple-system,
        BlinkMacSystemFont,
        "Segoe UI",
        "Hiragino Kaku Gothic ProN",
        "Yu Gothic",
        Meiryo,
        sans-serif;
    color:#222;
    background:#f4f6f4;
}

h1 {
    margin:0 0 25px;
    font-size:26px;
}

.average-card {
    background:white;
    border:1px solid #d8ddd8;
    border-radius:14px;
    padding:24px;
    box-shadow:
        0 4px 18px rgba(0,0,0,.08);
}

.average-row {
    display:flex;
    justify-content:space-between;
    align-items:center;
    gap:20px;
    padding:13px 0;
    border-bottom:1px solid #e2e5e2;
}

.average-row:last-child {
    border-bottom:none;
}

.average-label {
    font-weight:600;
}

.average-value {
    font-weight:700;
    text-align:right;
    white-space:nowrap;
}

.average-space {
    height:10px;
}

</style>

</head>

<body>

<h1>${year}年 平均</h1>

<div class="average-card">

    <div class="average-row">
        <span class="average-label">
            所得
        </span>
        <span class="average-value">
            ${money(normalIncome)}円
        </span>
    </div>

    <div class="average-row">
        <span class="average-label">
            所得(賞与込み)
        </span>
        <span class="average-value">
            ${money(incomeWithBonus)}円
        </span>
    </div>

    <div class="average-space"></div>

    <div class="average-row">
        <span class="average-label">
            総支給
        </span>
        <span class="average-value">
            ${money(normalGross)}円
        </span>
    </div>

    <div class="average-row">
        <span class="average-label">
            総支給(賞与込み)
        </span>
        <span class="average-value">
            ${money(grossWithBonus)}円
        </span>
    </div>

    <div class="average-space"></div>

    <div class="average-row">
        <span class="average-label">
            支出
        </span>
        <span class="average-value">
            ${money(averageExpense)}円
        </span>
    </div>

    <div class="average-row">
        <span class="average-label">
            収支
        </span>
        <span class="average-value">
            ${money(averageBalance)}円
        </span>
    </div>

    <div class="average-space"></div>

    <div class="average-row">
        <span class="average-label">
            電気
        </span>
        <span class="average-value">
            ${money(averageElectricity)}円
        </span>
    </div>

    <div class="average-row">
        <span class="average-label">
            ガス
        </span>
        <span class="average-value">
            ${money(averageGas)}円
        </span>
    </div>

    <div class="average-row">
        <span class="average-label">
            水道
        </span>
        <span class="average-value">
            ${money(averageWater)}円
        </span>
    </div>

</div>

</body>
</html>
    `);

    averageWindow.document.close();
}

/* =========================================================
   年詳細
========================================================= */

function showYearDetail(year) {

    currentPage = "years";

    document
        .querySelectorAll(".nav-btn[data-page]")
        .forEach(btn => {
            btn.classList.toggle(
                "active",
                btn.dataset.page === "years"
            );
        });

    /*
     * 通常月
     */
    const normalRows =
        getYearMonths(year).map(month => ({
            type: "normal",
            yearMonth: month.yearMonth,
            data: month
        }));

    /*
     * 賞与
     *
     * 通常月が存在しなくても表示する。
     */
    const bonusRows =
        (data.bonuses || [])
            .filter(
                bonus =>
                    bonus.yearMonth.startsWith(
                        year + "-"
                    )
            )
            .map(bonus => ({
                type: "bonus",
                yearMonth: bonus.yearMonth,
                data: bonus
            }));

    /*
     * 通常+賞与を年月順に統合
     *
     * 同じ年月なら
     * 通常 → 賞与
     */
    const rows = [
        ...normalRows,
        ...bonusRows
    ].sort((a, b) => {

        const dateCompare =
            a.yearMonth.localeCompare(
                b.yearMonth
            );

        if (dateCompare !== 0) {
            return dateCompare;
        }

        if (a.type === b.type) {
            return 0;
        }

        return a.type === "normal"
            ? -1
            : 1;
    });

    let html = `

        <div class="detail-header">

            <div class="detail-year">
                ${year}年
            </div>

            <button
                class="back-btn"
                onclick="showPage('years')"
            >
                ← 年別へ戻る
            </button>

        </div>

        <div class="month-table-wrapper">

            <table class="month-table">

                <thead>
                    <tr>
                        <th>月</th>
                        <th>所得</th>
                        <th>総支給</th>
                        <th>支出</th>
                        <th>資産</th>
                        <th>ローン</th>
                        <th>電気</th>
                        <th>ガス</th>
                        <th>水道</th>
                    </tr>
                </thead>

                <tbody>
    `;

    if (!rows.length) {

        html += `

            <tr>
                <td
                    colspan="9"
                    style="
                        text-align:center;
                        color:var(--sub-text);
                        padding:25px;
                    "
                >
                    記録がありません。
                </td>
            </tr>

        `;

    } else {

        rows.forEach(row => {

            /*
             * ==========================
             * 賞与
             * ==========================
             */
            if (row.type === "bonus") {

                const bonus = row.data;

                html += `

                    <tr>

                        <td>
    <span
        class="year-link"
        style="
            color:red;
            font-weight:700;
            cursor:pointer;
        "
        onclick="editBonus(${bonus.id})"
    >
        ${bonus.yearMonth}
    </span>
</td>

                        <td>
                            <span
                                style="
                                    color:red;
                                    font-weight:700;
                                "
                            >
                                ${money(
                                    bonus.income
                                )}
                            </span>
                        </td>

                        <td>
                            <span
                                style="
                                    color:red;
                                    font-weight:700;
                                "
                            >
                                ${money(
                                    bonus.grossPay
                                )}
                            </span>
                        </td>

                        <td>-</td>
                        <td>-</td>
                        <td>-</td>
                        <td>-</td>
                        <td>-</td>
                        <td>-</td>

                    </tr>

                `;

                return;
            }

            /*
             * ==========================
             * 通常月
             * ==========================
             */

            const month = row.data;

            html += `

                <tr>

                    <td>

                        <span
                            class="year-link"
                            onclick="editMonth(
                                '${month.yearMonth}'
                            )"
                        >
                            ${month.yearMonth}
                        </span>

                    </td>

                    <td>
                        ${money(
                            month.income
                        )}
                    </td>

                    <td>
                        ${money(
                            month.grossPay
                        )}
                    </td>

                    <td>
                        ${money(
                            month.expense
                        )}
                    </td>

                    <td
                        class="asset-cell"
                    >
                        ${money(
                            month.asset
                        )}
                    </td>

                    <td>
                        ${money(
                            month.loan
                        )}
                    </td>

                    <td>
                        ${money(
                            month.electricity?.amount || 0
                        )}
                    </td>

                    <td>
                        ${money(
                            month.gas?.amount || 0
                        )}
                    </td>

                    <td>
                        ${money(
                            month.water?.amount || 0
                        )}
                    </td>

                </tr>

            `;
        });
    }

    html += `

                </tbody>

            </table>

        </div>

    `;

    document
        .getElementById("main")
        .innerHTML = html;
}

/* =========================================================
   空の月
========================================================= */

function emptyMonth() {

    return {

        yearMonth: "",

        income: 0,

        overtimeHours: 0,

        overtimePay: 0,

        grossPay: 0,

        expense: 0,

        temporaryIncome: [
            {
                memo: "",
                amount: 0
            }
        ],

        expenses: [
            {
                memo: "",
                amount: 0
            }
        ],

        asset: 0,

        loan: 0,

        electricity: {
            kwh: 0,
            amount: 0
        },

        gas: {
            volume: 0,
            amount: 0
        },

        water: {
            volume: 0,
            amount: 0
        },

        saving: 0,

        memo: ""

    };

}


/* =========================================================
追加画面
========================================================= */

function renderBonusPage() {

    document.getElementById("main").innerHTML = `

        <h1 class="page-title">
            賞与入力
        </h1>

        <div class="form-card">

            <div class="add-form-grid">

                <div class="form-group">
                    <label>年月</label>

                    <input
                        id="bonus-yearMonth"
                        type="month"
                    >
                </div>

                <div class="form-group">
                    <label>賞与 所得</label>

                    <input
                        id="bonus-income"
                        type="number"
                        value="0"
                    >
                </div>

                <div class="form-group">
                    <label>賞与 総支給</label>

                    <input
                        id="bonus-grossPay"
                        type="number"
                        value="0"
                    >
                </div>

                <div class="form-group">
                    <label>メモ</label>

                    <input
                        id="bonus-memo"
                        type="text"
                        placeholder="メモ"
                    >
                </div>

                <div class="form-group">
                    <label>金融資産1</label>

                    <input
                        id="bonus-financialAsset1"
                        type="number"
                        value="0"
                    >
                </div>

                <div class="form-group">
                    <label>金融資産2</label>

                    <input
                        id="bonus-financialAsset2"
                        type="number"
                        value="0"
                    >
                </div>

            </div>

            <div class="form-actions">

                <button
                    class="secondary-btn"
                    onclick="showPage('add')"
                >
                    戻る
                </button>

                <button
                    class="primary-btn"
                    onclick="saveBonus()"
                >
                    賞与を保存
                </button>

            </div>

        </div>
    `;
}

function renderAddPage(

    monthKey = null
) {

    editingMonthKey =
        monthKey;

    const month =
        monthKey &&
        data.months[monthKey]

            ? JSON.parse(
                JSON.stringify(
                    data.months[
                        monthKey
                    ]
                )
            )

            : emptyMonth();


    if (
        !month.temporaryIncome ||
        !month.temporaryIncome.length
    ) {

        month.temporaryIncome = [
            {
                memo: "",
                amount: 0
            }
        ];

    }


    if (
        !month.expenses ||
        !month.expenses.length
    ) {

        month.expenses = [
            {
                memo: "",
                amount: 0
            }
        ];

    }


    const title =
        monthKey
            ? `${month.yearMonth} 編集`
            : "追加";


    /*
       ここから追加画面

       1段目
       年月 / 所得 / 残業時間 / 残業代

       2段目
       総支給 / 支出

       3段目
       臨時収入

       4段目
       出費

       5段目
       資産 / ローン / 積立

       6段目
       電気 / ガス / 水道

       その下
       メモ
    */

    document
        .getElementById("main")
        .innerHTML = `


<div style="
    display:flex;
    align-items:center;
    gap:12px;
    margin-bottom:15px;
">

    <input
        id="bonusModeCheck"
        type="checkbox"
        style="
            width:24px;
            height:24px;
            cursor:pointer;
        "
    >

    <span>
        👈賞与入力画面へ
    </span>
</div>

        <div class="form-card">

            <!-- ================================
                 1段目
            ================================= -->

            <div class="add-form-grid">

                <div class="form-group">
                    <label>年月</label>

                    <input
                        id="f-yearMonth"
                        type="month"
                        value="${month.yearMonth}"
                    >
                </div>

                <div class="form-group">
                    <label>所得</label>

                    <input
                        id="f-income"
                        type="number"
                        value="${month.income}"
                    >
                </div>

                <div class="form-group">
                    <label>残業時間(h)</label>

                    <input
                        id="f-overtimeHours"
                        type="number"
                        step="0.1"
                        value="${month.overtimeHours}"
                    >
                </div>

                <div class="form-group">
                    <label>残業代</label>

                    <input
                        id="f-overtimePay"
                        type="number"
                        value="${month.overtimePay}"
                    >

<div class="overtime-per-hour">
    1時間あたり
    <span id="f-overtimePerHour">0</span> 円
</div>

                </div>


                <!-- ================================
                     2段目
                ================================= -->

                <div class="form-group">
                    <label>総支給</label>

                    <input
                        id="f-grossPay"
                        type="number"
                        value="${month.grossPay}"
                    >
                </div>

                                <div class="form-group">
                    <label>支出</label>

                    <input
                        id="f-expense"
                        type="number"
                        value="${month.expense}"
                    >
                </div>

                <div class="form-group">
                    <label>金融資産1</label>

                    <input
                        id="f-financialAsset1"
                        type="number"
                        value="${month.financialAsset1 ?? 0}"
                    >
                </div>

                <div class="form-group">
                    <label>金融資産2</label>

                    <input
                        id="f-financialAsset2"
                        type="number"
                        value="${month.financialAsset2 ?? 0}"
                    >
                </div>

            </div>


            <!-- ================================
                 3段目 臨時収入
            ================================= -->

            <div class="detail-editor-section">

                <div class="detail-editor-title">
                    臨時収入
                </div>

                <div
                    id="temporaryIncomeEditor"
                    class="line-editor"
                ></div>

            </div>


            <!-- ================================
                 4段目 出費
            ================================= -->

            <div class="detail-editor-section">

                <div class="detail-editor-title">
                    出費
                </div>

                <div
                    id="expenseEditor"
                    class="line-editor"
                ></div>

            </div>


            <!-- ================================
                 5段目 資産・ローン・積立
            ================================= -->

            <div class="detail-editor-section">

                <div class="detail-editor-title">
                    資産・ローン・積立
                </div>

                <div class="three-column-row">

                    <div class="form-group">

                        <label>
                            資産
                        </label>

                        <input
                            id="f-asset"
                            type="number"
                            value="${month.asset}"
                        >

                    </div>

                    <div class="form-group">

                        <label>
                            ローン
                        </label>

                        <input
                            id="f-loan"
                            type="number"
                            value="${month.loan}"
                        >

                    </div>

                    <div class="form-group">

                        <label>
                            積立
                        </label>

                        <input
                            id="f-saving"
                            type="number"
                            value="${month.saving}"
                        >

                    </div>

                </div>

            </div>


            <!-- ================================
                 6段目
            ================================= -->

            
<div class="detail-editor-section">

    <div class="detail-editor-title">
        光熱費
    </div>

    <div class="four-column-row">

        <!-- 電気 -->

        <div class="form-group">
            <div class="utility-inner">

                <div class="form-group">
                    <label>電気量 (kwh)</label>
                    <input
                        id="f-electricity-kwh"
                        type="number"
                        step="0.1"
                        placeholder="kWh"
                        value="${month.electricity.kwh}"
                    >
                </div>

                <div class="form-group">
                    <label>電気</label>
                    <input
                        id="f-electricity-amount"
                        type="number"
                        placeholder="金額"
                        value="${month.electricity.amount}"
                    >
                </div>

            </div>
        </div>

        <!-- ガス -->

        <div class="form-group">
            <div class="utility-inner">

                <div class="form-group">
                    <label>ガス量 (㎥)</label>
                    <input
                        id="f-gas-volume"
                        type="number"
                        step="0.1"
                        placeholder="㎥"
                        value="${month.gas.volume}"
                    >
                </div>

                <div class="form-group">
                    <label>ガス</label>
                    <input
                        id="f-gas-amount"
                        type="number"
                        placeholder="金額"
                        value="${month.gas.amount}"
                    >
                </div>

            </div>
        </div>

        <!-- 水道 -->

        <div class="form-group">
            <div class="utility-inner">

                <div class="form-group">
                    <label>水道量 (㎥)</label>
                    <input
                        id="f-water-volume"
                        type="number"
                        step="0.1"
                        placeholder="㎥"
                        value="${month.water.volume}"
                    >
                </div>

                <div class="form-group">
                    <label>水道</label>
                    <input
                        id="f-water-amount"
                        type="number"
                        placeholder="金額"
                        value="${month.water.amount}"
                    >
                </div>

            </div>
        </div>

    </div>

</div>


            <!-- ================================
                 メモ
            ================================= -->

            <div class="memo-area">

                <div class="detail-editor-title">
                    メモ
                </div>

                <textarea
                    id="f-memo"
                    placeholder="メモを入力"
                >${escapeHtml(
                    month.memo || ""
                )}</textarea>

            </div>


            <!-- ================================
                 ボタン
            ================================= -->

            <div class="form-actions">

                <button
                    class="secondary-btn"
                    onclick="showPage('years')"
                >
                    キャンセル
                </button>

                <button
                    class="primary-btn"
                    onclick="saveMonthForm()"
                >
                    保存
                </button>

<button
    class="danger-btn"
    id="deleteMonthBtn"
    onclick="deleteEditingMonth()"
>
    削除
</button>

            </div>

        </div>

        `;

    const deleteMonthBtn =
        document.getElementById(
            "deleteMonthBtn"
        );

    if (deleteMonthBtn) {
        deleteMonthBtn.classList.toggle(
            "hidden",
            !editingMonthKey
        );
    }

    renderLineEditor(
        "temporaryIncomeEditor",

        month.temporaryIncome,
        "temporaryIncome"
    );


    renderLineEditor(
        "expenseEditor",
        month.expenses,
        "expenses"
    );

const overtimeHoursInput =
    document.getElementById(
        "f-overtimeHours"
    );

const overtimePayInput =
    document.getElementById(
        "f-overtimePay"
    );

const overtimePerHour =
    document.getElementById(
        "f-overtimePerHour"
    );

function updateOvertimePerHour() {

    const hours =
        Number(
            overtimeHoursInput.value
        ) || 0;

    const pay =
        Number(
            overtimePayInput.value
        ) || 0;

    if (hours > 0) {

        overtimePerHour.textContent =
            Math.round(
                pay / hours
            ).toLocaleString();

    } else {

        overtimePerHour.textContent =
            "0";
    }
}

overtimeHoursInput.addEventListener(
    "input",
    updateOvertimePerHour
);

overtimePayInput.addEventListener(
    "input",
    updateOvertimePerHour
);

const bonusModeCheck =
    document.getElementById(
        "bonusModeCheck"
    );

bonusModeCheck.addEventListener(
    "change",
    () => {
        if (bonusModeCheck.checked) {
            renderBonusPage();
        }
    }
);

updateOvertimePerHour();

}


/* =========================================================
   臨時収入・出費の明細
========================================================= */

function renderLineEditor(
    containerId,
    items,
    type
) {

    const container =
        document.getElementById(
            containerId
        );

    container.innerHTML = "";


    items.forEach(
        (item,index) => {

            const row =
                document.createElement(
                    "div"
                );

            row.className =
                "line-row";


            row.innerHTML = `

                <input
                    type="text"
                    placeholder="メモ"
                    value="${escapeHtml(
                        item.memo || ""
                    )}"
                    data-line-type="${type}"
                    data-field="memo"
                >

                <input
                    type="number"
                    placeholder="金額"
                    value="${Number(
                        item.amount || 0
                    )}"
                    data-line-type="${type}"
                    data-field="amount"
                >

                <div class="line-actions">

                    <button
                        type="button"
                        class="add-line-small"
                        onclick="addEditorLine('${type}')"
                    >
                        追加
                    </button>

                    <button
                        type="button"
                        class="delete-line-small"
                        onclick="this.closest('.line-row').remove()"
                    >
                        削除
                    </button>

                </div>

            `;


            container.appendChild(row);

        }
    );

}


/*
   「追加」を押すと、
   その種類の明細を1行追加する。
*/
function addEditorLine(type) {
    const containerId =
        type === "temporaryIncome"
            ? "temporaryIncomeEditor"
            : "expenseEditor";

    const container =
        document.getElementById(containerId);

    if (!container) {
        console.error(
            "明細入力欄が見つかりません:",
            containerId
        );
        return;
    }

    const currentCount =
        container.querySelectorAll(".line-row").length;

    if (currentCount >= 4) {
        alert(
            (type === "temporaryIncome"
                ? "臨時収入"
                : "出費") +
            "は最大4件までです。"
        );
        return;
    }

    const row =
        document.createElement("div");

    row.className = "line-row";

    row.innerHTML = `
        <input
            type="text"
            placeholder="メモ"
            data-line-type="${type}"
            data-field="memo"
        >
        <input
            type="number"
            placeholder="金額"
            value="0"
            data-line-type="${type}"
            data-field="amount"
        >
        <div class="line-actions">
            <button
                type="button"
                class="add-line-small"
                onclick="addEditorLine('${type}')"
            >
                追加
            </button>
            <button
                type="button"
                class="delete-line-small"
                onclick="this.closest('.line-row').remove()"
            >
                削除
            </button>
        </div>
    `;

    container.appendChild(row);
}

/* =========================================================
   明細取得
========================================================= */

function collectLineEditor(type) {

    const containerId =
        type === "temporaryIncome"
            ? "temporaryIncomeEditor"
            : "expenseEditor";


    const container =
        document.getElementById(
            containerId
        );


    const rows =
        [
            ...container.querySelectorAll(
                ".line-row"
            )
        ];


    return rows

        .map(row => {

            const memo =
                row
                    .querySelector(
                        '[data-field="memo"]'
                    )
                    .value;

            const amount =
                Number(
                    row
                        .querySelector(
                            '[data-field="amount"]'
                        )
                        .value || 0
                );


            return {
                memo,
                amount
            };

        })

        .filter(
            item =>
                item.memo.trim() !== "" ||
                item.amount !== 0
        );

}

/* =========================================================
   月削除
========================================================= */

async function deleteEditingMonth() {

    if (!editingMonthKey) {
        return;
    }

    const confirmed =
        confirm(
            editingMonthKey +
            " の記録を削除します。\n\n" +
            "この操作は元に戻せません。\n" +
            "削除してよろしいですか?"
        );

    if (!confirmed) {
        return;
    }

    try {

        delete data.months[
            editingMonthKey
        ];

        await saveData();

        await deleteDraft();

        const deletedYear =
            editingMonthKey.substring(
                0,
                4
            );

        editingMonthKey = null;

showYearDetail(
    deletedYear
);

    } catch (error) {

        console.error(
            "月データの削除に失敗しました。",
            error
        );

        alert(
            "削除に失敗しました。\n\n" +
            "データは変更されていません。"
        );
    }
}


/* =========================================================
   月保存
========================================================= */

async function saveMonthForm() {

    const yearMonth =
        document
        .getElementById(
            "f-yearMonth"
        )
        .value;

    if (!yearMonth) {
        alert(
            "年月を入力してください。"
        );
        return;
    }

    const month = {
        yearMonth,
        income:
            Number(
                document
                .getElementById(
                    "f-income"
                )
                .value || 0
            ),
        overtimeHours:
            Number(
                document
                .getElementById(
                    "f-overtimeHours"
                )
                .value || 0
            ),
        overtimePay:
            Number(
                document
                .getElementById(
                    "f-overtimePay"
                )
                .value || 0
            ),
        grossPay:
            Number(
                document
                .getElementById(
                    "f-grossPay"
                )
                .value || 0
            ),
        expense:
            Number(
                document
                .getElementById(
                    "f-expense"
                )
                .value || 0
            ),

        financialAsset1:
            Number(
                document
                .getElementById(
                    "f-financialAsset1"
                )
                .value || 0
            ),

        financialAsset2:
            Number(
                document
                .getElementById(
                    "f-financialAsset2"
                )
                .value || 0
            ),

        temporaryIncome:
            collectLineEditor(
                "temporaryIncome"
            ),

        expenses:
            collectLineEditor(
                "expenses"
            ),

        asset:
            Number(
                document
                .getElementById(
                    "f-asset"
                )
                .value || 0
            ),

        loan:
            Number(
                document
                .getElementById(
                    "f-loan"
                )
                .value || 0
            ),

        electricity: {
            kwh:
                Number(
                    document
                    .getElementById(
                        "f-electricity-kwh"
                    )
                    .value || 0
                ),
            amount:
                Number(
                    document
                    .getElementById(
                        "f-electricity-amount"
                    )
                    .value || 0
                )
        },

        gas: {
            volume:
                Number(
                    document
                    .getElementById(
                        "f-gas-volume"
                    )
                    .value || 0
                ),
            amount:
                Number(
                    document
                    .getElementById(
                        "f-gas-amount"
                    )
                    .value || 0
                )
        },

        water: {
            volume:
                Number(
                    document
                    .getElementById(
                        "f-water-volume"
                    )
                    .value || 0
                ),
            amount:
                Number(
                    document
                    .getElementById(
                        "f-water-amount"
                    )
                    .value || 0
                )
        },

        
        saving:
            Number(
                document
                .getElementById(
                    "f-saving"
                )
                .value || 0
            ),

        memo:
            document
            .getElementById(
                "f-memo"
            )
            .value
    };
/*
         * 正式データの更新前に退避する。
         * 年月変更による旧データの削除も含めて復元できるようにする。
         */
        const previousMonths = {
            ...data.months
        };

        /*
         * 正式データを更新する。
         *
         * 編集中に年月を変更した場合は、
         * 変更前のキーを削除してから
         * 新しい年月のキーへ保存する。
         */
        if (
            editingMonthKey &&
            editingMonthKey !== yearMonth
        ) {
            delete data.months[editingMonthKey];
        }

        data.months[yearMonth] = month;

        /*
         * 正式保存。
         * 失敗した場合は更新前のデータを復元し、
         * draftは削除しない。
         */
        try {
            await saveData();

        } catch (error) {

            data.months = previousMonths;

            console.error(
                "月データの保存に失敗しました。",
                error
            );

            alert(
                "保存に失敗しました。\n\n" +
                "更新前のデータをメモリ上で復元しました。\n" +
                "入力内容を保持するため、" +
                "一時保存データは削除していません。"
            );

            return;
        }

    /*
     * 正式保存成功後だけ、
     * draftを削除する。
     */
    try {
        await deleteDraft();
    } catch (error) {
        /*
         * 正式データの保存自体は成功しているため、
         * draft削除失敗では保存失敗扱いにしない。
         */
        console.error(
            "draftの削除に失敗しました。",
            error
        );

        alert(
            "データは保存されましたが、" +
            "一時保存データの削除に失敗しました。"
        );
    }

    editingMonthKey =
        null;

    /*
     * 正式保存成功後のみ表示。
     */
    
    showYearDetail(
        yearMonth.substring(0, 4)
    );
}

async function saveBonus() {

    const yearMonth =
        document.getElementById(
            "bonus-yearMonth"
        ).value;

    const income =
        Number(
            document.getElementById(
                "bonus-income"
            ).value || 0
        );

    const grossPay =
        Number(
            document.getElementById(
                "bonus-grossPay"
            ).value || 0
        );

    const financialAsset1 =
        Number(
            document.getElementById(
                "bonus-financialAsset1"
            ).value || 0
        );

    const financialAsset2 =
        Number(
            document.getElementById(
                "bonus-financialAsset2"
            ).value || 0
        );

    const memo =
        document.getElementById(
            "bonus-memo"
        ).value.trim();

    if (!yearMonth) {
        alert("年月を入力してください。");
        return;
    }

    if (
        income === 0 &&
        grossPay === 0 &&
        financialAsset1 === 0 &&
        financialAsset2 === 0
    ) {
        alert(
            "賞与の所得・総支給・金融資産1・金融資産2のいずれかを入力してください。"
        );
        return;
    }

    if (!data.bonuses) {
        data.bonuses = [];
    }

    data.bonuses.push({
        id: Date.now(),
        yearMonth,
        income,
        grossPay,
        financialAsset1,
        financialAsset2,
        memo
    });

    try {

        await saveData();

        showYearDetail(
            yearMonth.substring(0, 4)
        );

    } catch (error) {

        console.error(
            "賞与保存エラー:",
            error
        );

        alert(
            "賞与の保存に失敗しました。\n\n" +
            error.message
        );
    }
}

/* =========================================================
   賞与編集
========================================================= */

function editBonus(bonusId) {

    const bonus =
        (data.bonuses || [])
            .find(
                item =>
                    String(item.id) ===
                    String(bonusId)
            );

    if (!bonus) {
        alert("賞与データが見つかりません。");
        return;
    }

    document.getElementById("main").innerHTML = `

        <h1 class="page-title">
            賞与編集
        </h1>

        <div class="form-card">

            <div class="add-form-grid">

                <div class="form-group">

                    <label>年月</label>

                    <input
                        id="bonus-yearMonth"
                        type="month"
                        value="${bonus.yearMonth}"
                    >

                </div>

                <div class="form-group">

                    <label>賞与 所得</label>

                    <input
                        id="bonus-income"
                        type="number"
                        value="${Number(
                            bonus.income || 0
                        )}"
                    >

                </div>

                <div class="form-group">

                    <label>賞与 総支給</label>

                    <input
                        id="bonus-grossPay"
                        type="number"
                        value="${Number(
                            bonus.grossPay || 0
                        )}"
                    >

                </div>

                <div class="form-group">

                    <label>メモ</label>

                    <input
                        id="bonus-memo"
                        type="text"
                        value="${escapeHtml(
                            bonus.memo || ""
                        )}"
                    >

                </div>

                <div class="form-group">

                    <label>金融資産1</label>

                    <input
                        id="bonus-financialAsset1"
                        type="number"
                        value="${Number(
                            bonus.financialAsset1 || 0
                        )}"
                    >

                </div>

                <div class="form-group">

                    <label>金融資産2</label>

                    <input
                        id="bonus-financialAsset2"
                        type="number"
                        value="${Number(
                            bonus.financialAsset2 || 0
                        )}"
                    >

                </div>

            </div>

            <div class="form-actions">

                <button
                    class="secondary-btn"
                    onclick="
                        showYearDetail(
                            '${bonus.yearMonth.substring(0, 4)}'
                        )
                    "
                >
                    戻る
                </button>

                <button
                    class="primary-btn"
                    onclick="
                        updateBonus(${bonus.id})
                    "
                >
                    保存
                </button>

                <button
                    class="danger-btn"
                    onclick="
                        deleteBonus(${bonus.id})
                    "
                >
                    削除
                </button>

            </div>

        </div>

    `;
}

async function updateBonus(bonusId) {

    const bonus =
        (data.bonuses || []).find(
            item =>
                String(item.id) ===
                String(bonusId)
        );

    if (!bonus) {
        alert("賞与データが見つかりません。");
        return;
    }

    const yearMonth =
        document.getElementById(
            "bonus-yearMonth"
        ).value;

    const income =
        Number(
            document.getElementById(
                "bonus-income"
            ).value || 0
        );

    const grossPay =
        Number(
            document.getElementById(
                "bonus-grossPay"
            ).value || 0
        );

    const financialAsset1 =
        Number(
            document.getElementById(
                "bonus-financialAsset1"
            ).value || 0
        );

    const financialAsset2 =
        Number(
            document.getElementById(
                "bonus-financialAsset2"
            ).value || 0
        );

    const memo =
        document.getElementById(
            "bonus-memo"
        ).value.trim();

    if (!yearMonth) {
        alert("年月を入力してください。");
        return;
    }

    // 更新前の賞与データを退避
    const originalBonus = { ...bonus };

    // 入力内容を反映
    bonus.yearMonth = yearMonth;
    bonus.income = income;
    bonus.grossPay = grossPay;
    bonus.financialAsset1 = financialAsset1;
    bonus.financialAsset2 = financialAsset2;
    bonus.memo = memo;

    try {
        await saveData();

        showYearDetail(
            yearMonth.substring(0, 4)
        );

    } catch (error) {

        // 更新前のデータ構造と値を復元
        Object.keys(bonus).forEach(key => {
            if (
                !Object.prototype.hasOwnProperty.call(
                    originalBonus,
                    key
                )
            ) {
                delete bonus[key];
            }
        });

        Object.assign(
            bonus,
            originalBonus
        );

        console.error(
            "賞与更新エラー:",
            error
        );

        alert(
            "賞与の更新に失敗しました。\n\n" +
            "更新前のデータをメモリ上で復元しました。\n" +
            "保存先とファイルの状態を確認してください。\n\n" +
            error.message
        );
    }
}

async function deleteBonus(bonusId) {

    if (
        !confirm(
            "この賞与を削除しますか?"
        )
    ) {
        return;
    }

    const bonus =
        (data.bonuses || [])
            .find(
                item =>
                    String(item.id) ===
                    String(bonusId)
            );

    if (!bonus) {
        alert(
            "賞与データが見つかりません。"
        );
        return;
    }

    const deletedYear =
        bonus.yearMonth.substring(
            0,
            4
        );

    data.bonuses =
        (data.bonuses || [])
            .filter(
                item =>
                    String(item.id) !==
                    String(bonusId)
            );

    try {

        await saveData();

        showYearDetail(
            deletedYear
        );

    } catch (error) {

        console.error(
            "賞与削除エラー:",
            error
        );

        alert(
            "賞与の削除に失敗しました。\n\n" +
            error.message
        );
    }
}

/* =========================================================
   月編集
========================================================= */

function editMonth(
    yearMonth
) {

    renderAddPage(
        yearMonth
    );

}

/* =========================================================
   年メモ
========================================================= */

function openYearMemo(
    year
) {

    const existing =
        (data.yearMemos || {})[year] ||
        "";

    document
        .getElementById(
            "modalCard"
        )
        .innerHTML = `

        <div class="modal-title">
            ${year}年 メモ
        </div>

        <textarea
            id="yearMemoInput"
            style="min-height:260px;"
        >${escapeHtml(
            existing
        )}</textarea>

        <div class="modal-actions">

            <button
                class="secondary-btn"
                onclick="closeModal()"
            >
                キャンセル
            </button>

            <button
                class="primary-btn"
                onclick="saveYearMemo('${year}')"
            >
                保存
            </button>

        </div>

    `;

    document
        .getElementById(
            "modal"
        )
        .classList
        .remove("hidden");
}


async function saveYearMemo(
    year
) {

    data.yearMemos[year] =
        document
            .getElementById(
                "yearMemoInput"
            )
            .value;

    await saveData();

    closeModal();

    // 年別画面を即時更新
    renderYears();
}

function closeModal() {

    document
        .getElementById(
            "modal"
        )
        .classList
        .add("hidden");

}

/* =========================================================
   設定
========================================================= */

function renderSettings() {

    document
        .getElementById("main")
        .innerHTML = `

        <div class="setting-section">

            <h3>
                基本設定
            </h3>

            <div class="setting-row">

                <div>
                    ダークモード
                </div>

                <div class="setting-control">

                    <input
                        id="darkModeSetting"
                        type="checkbox"
                        ${
                            settings.darkMode
                                ? "checked"
                                : ""
                        }
                    >

                </div>

            </div>

            <div class="setting-row">

                <div>
                    イメージカラー
                </div>

                <div class="setting-control">

                    <input
                        id="mainColorSetting"
                        type="color"
                        value="${settings.mainColor}"
                    >

                </div>

            </div>

            <div class="setting-row">
<div>
タイトル変更
</div>
<div class="setting-control">
<input
id="appTitleSetting"
type="text"
value="${escapeHtml(
settings.appTitle || "支出管理"
)}"
placeholder="支出管理"
>
</div>
</div>

        </div>


        <div class="setting-section">

            <h3>
                文字設定
            </h3>


            ${fontSettingRow(
                "heading",
                "見出し",
                settings.fontSizes.heading
            )}

            ${fontSettingRow(
    "year",
    "年別",
    settings.fontSizes.year
)}

${fontSettingRow(
    "month",
    "月別",
    settings.fontSizes.month
)}

${fontSettingRow(
    "add",
    "追加",
    settings.fontSizes.add
)}

            ${fontSettingRow(
                "setting",
                "設定",
                settings.fontSizes.setting
            )}

            ${fontSettingRow(
                "description",
                "説明",
                settings.fontSizes.description
            )}

        </div>

<div class="form-actions">

            <button
                class="primary-btn"
                onclick="saveSettingPage()"
            >
                設定を保存
            </button>

        </div>

        <div class="form-actions">

            <button
                class="secondary-btn"
                onclick="showPage('ods')"
            >
                バックアップ
            </button>

        </div>

    `;


    [
    "heading",
    "year",
    "month",
    "add",
    "setting",
    "description"
]

    .forEach(type => {

        const input =
            document.getElementById(
                "font-" + type
            );

        const output =
            document.getElementById(
                "font-" +
                type +
                "-value"
            );

        input.addEventListener(
            "input",
            () => {

                output.textContent =
                    input.value +
                    "px";

            }
        );

    });

[
    "heading",
    "year",
    "month",
    "add",
    "setting",
    "description"
].forEach(type => {
    const select = document.getElementById(
        "font-family-" + type
    );

    if (!select) {
        console.warn(
            "フォント選択欄が見つかりません:",
            type
        );
        return;
    }

    const fontVariables = {
        heading: "--heading-font",
        year: "--year-font",
        month: "--month-font",
        add: "--add-font",
        setting: "--setting-font",
        description: "--description-font"
    };

    const fontFamilies = {
        "yu-gothic": '"Yu Gothic", YuGothic, sans-serif',
        "meiryo": 'Meiryo, sans-serif',
        "biz-udp-gothic": '"BIZ UDPGothic", "BIZ UDPゴシック", sans-serif',
        "biz-udp-mincho": '"BIZ UDPMincho", "BIZ UDP明朝", serif',
        "ms-p-gothic": '"MS PGothic", "MS Pゴシック", sans-serif',
        "ms-p-mincho": '"MS PMincho", "MS P明朝", serif'
    };

    select.value =
        settings.fontFamilies?.[type] || "yu-gothic";

    document.documentElement.style.setProperty(
        fontVariables[type],
        fontFamilies[select.value] ||
            fontFamilies["yu-gothic"]
    );

    // フォント変更時の処理
    select.addEventListener("change", async () => {
        const selectedFont = select.value;

        settings.fontFamilies =
            settings.fontFamilies || {};

        settings.fontFamilies[type] = selectedFont;

        document.documentElement.style.setProperty(
            fontVariables[type],
            fontFamilies[selectedFont] ||
                fontFamilies["yu-gothic"]
        );

        if (!directoryHandle) {
            console.warn(
                "保存先が未設定のため、フォント設定は未保存です。"
            );
            return;
        }

        try {
            await saveSettings();
        } catch (error) {
            console.error(
                "フォント設定の保存に失敗しました。",
                error
            );

            alert(
                "フォント設定を保存できませんでした。\n" +
                "保存先を確認してください。"
            );
        }
    });
});
}

function renderDescription() {
    document.getElementById("main").innerHTML = `
        <h1 class="page-title description-font">
            アプリ説明
        </h1>

        <div class="description-card description-font">

            <h2>1. アプリの概要</h2>
            <p>
                毎月の収入・支出・資産・光熱費などを記録し、
                年別・月別・一覧で確認するためのアプリです。
                通常月と賞与は別々のデータとして管理します。
                入力データは指定フォルダに保存します。
            </p>

            <h2>2. 年別画面</h2>
            <p>
                年ごとの所得・総支給・支出・収支・資産・累計金融資産を確認できます。
                年をクリックすると、その年の月別詳細へ移動します。
                「平均」からは年間の月平均を確認できます。
            </p>
            <p>
                所得・総支給の年間集計には賞与も含まれます。
                支出は通常月の支出を集計します。
                前年比は前年の数値との比較です。
            </p>

            <h2>3. 収支と資産の計算</h2>
            <p>
                <strong>収支 = 所得 − 支出 − ローン − 積立</strong>
            </p>
            <p>
                臨時収入・出費は上記の収支計算には自動加算されません。
                賞与の所得は年間の所得に含まれますが、
                年間の収支は通常月の所得・支出・ローン・積立から計算します。
            </p>
            <p>
                「資産」は通常月に入力した資産の値です。
                「金融資産1」「金融資産2」は資産とは別に管理します。
                「累計金融資産」は、その年までに登録された通常月と賞与の
                金融資産1・2をそれぞれ合計した金額です。
                年別画面の累計金融資産をクリックすると、
                金融資産1・2の内訳と合計を確認できます。
            </p>

            <h2>4. 年メモとチェックボックス</h2>
            <p>
                年別画面の「メモ編集」欄にあるチェックボックスは、
                その年の年メモに文字が入力されているかを示す表示専用です。
                チェックの操作でメモを編集するものではありません。
                右隣の「編集」ボタンから年メモを入力・変更します。
                年メモは月ごとのメモとは別に保存されます。
            </p>

            <h2>5. 月別画面・一覧画面</h2>
            <p>
                年別画面で年をクリックすると、月ごとの記録や明細を確認できます。
                通常月と賞与は別の記録として表示されます。
                月別詳細から対象の記録を確認し、編集操作へ進みます。
            </p>
            <p>
                一覧画面では、通常月・賞与の記録を表形式でまとめて確認できます。
                年月の新しい順に表示され、年月の絞り込みやサイズ変更ができます。
                年月をクリックすると、該当する通常月または賞与の編集画面へ移動します。
                メモ欄の📃をクリックすると、メモの内容を確認できます。
            </p>

            <h2>6. 通常月の追加・編集</h2>
            <p>
                上部の「追加」から年月を選び、通常月のデータを入力します。
                所得・総支給・支出・資産・金融資産1・金融資産2・残業時間・残業代・
                ローン・積立・電気・ガス・水道の使用量と金額・メモを記録できます。
            </p>
            <p>
                所得は収支計算に使う金額、総支給は総支給額の記録です。
                金融資産1・2はそれぞれ独立した金額として管理します。
                電気・ガス・水道は使用量と料金を別々に入力します。
                メモには、その月の補足情報を記録できます。
            </p>
            <p>
                臨時収入と出費は、それぞれメモと金額を組み合わせて最大4件まで登録できます。
                追加ボタンで明細を増やし、不要な明細は削除できます。
                画面移動前の入力内容は一時保存・復元の対象となります。
                正式な保存と一時保存は別の処理です。
            </p>

            <h2>7. 賞与の入力・編集</h2>
            <p>
                賞与は通常月の入力欄に加算するのではなく、独立した記録として登録します。
                「追加」画面の「👈賞与入力画面へ」から賞与入力画面を開きます。
                年月・賞与の所得・賞与の総支給・メモ・金融資産1・金融資産2を入力し、
                保存ボタンで登録します。
            </p>
            <p>
                所得・総支給・金融資産1・金融資産2のすべてが0の場合は登録できません。
                いずれかの項目に値を入力してください。
                登録済みの賞与は、一覧画面で賞与の年月をクリックして編集できます。
                通常月と賞与は同じ年月でも別の記録として扱います。
            </p>

            <h2>8. 設定・背景画像</h2>
            <p>
                「設定」では、ダークモード、イメージカラー、タイトル、文字サイズ、
                フォントなどを変更できます。
                「背景画像」では背景画像・背景カラー・透過設定を調整できます。
                設定項目によっては保存先への保存が必要です。
            </p>

            <h2>9. ODSバックアップ</h2>
            <p>
                設定画面からODSバックアップを開き、
                「ODS出力」でデータを表計算ファイルに出力、
                「ODS読取り」でファイルのデータを取り込めます。
                通常月と賞与は別行で扱い、臨時収入・出費は各4組の固定列で管理します。
                年メモは月ごとのメモとは別の項目です。
            </p>
            <p>
                読取り時には、年月・数値・重複などの検査や上書き確認が行われます。
                読取り前には、念のためバックアップを作成してください。
                外部で編集したODSは、見出しや列の対応関係を確認してから取り込んでください。
            </p>

            <h2>10. 保存と一時保存</h2>
            <p>
                正式データは指定フォルダ内の expenditure-data.json、
                設定は settings.json、一時保存は expenditure-draft.json に保存します。
                起動時は指定フォルダを選択または復元してください。
                保存先が利用できない場合、保存や設定変更が正常に完了しないことがあります。
            </p>

            <h2>11. 編集場所の早見表</h2>
            <p>
                ・通常月の新規登録:上部の「追加」<br>
                ・通常月の修正:月別詳細または一覧画面から対象年月を選択<br>
                ・賞与の新規登録:「追加」から「👈賞与入力画面へ」<br>
                ・賞与の修正:一覧画面で賞与の年月を選択<br>
                ・年メモの修正:年別画面の「メモ編集」欄にある「編集」<br>
                ・設定の変更:上部の「設定」<br>
                ・ODSの読取り・出力:設定画面から「ODSバックアップ」
            </p>

        </div>

        <div class="form-actions description-font">
            <button
                class="secondary-btn description-font"
                onclick="showPage('years')"
            >
                年別画面へ戻る
            </button>
        </div>
    `;
}

/* =========================================================
   ODSバックアップページ
========================================================= */

function renderOdsPage() {
    document.getElementById("main").innerHTML = `
        <div class="ods-page">
            <h1 class="page-title">ODSバックアップ</h1>

            <p>
                アプリのデータをODS形式で出力・読取りできます。
                出力ファイル名は expenditure-backup.ods です。
                読取り前には、念のため現在のデータをバックアップしてください。
            </p>

            <div class="setting-section">
                <h3>ODS操作</h3>
                <div class="form-actions">
                    <button class="secondary-btn" onclick="importOds()">
                        ODS読取り
                    </button>
                    <button class="primary-btn" onclick="exportOds()">
                        ODS出力
                    </button>
                </div>
            </div>

            <div class="description-card">
                <h2>1. 出力形式と並び順</h2>
                <p>
                    先頭行は見出し、2行目以降は1レコードにつき1行です。
                    通常月と賞与は別レコードとして出力し、
                    年月の新しい順に並びます。同じ年月の場合は通常月が先、
                    賞与が後です。ファイルは「expenditure-backup.ods」
                    という名前で出力されます。
                </p>

                <h2>2. 36項目の列順</h2>
                <p>
                    ①年月 ②所得 ③総支給 ④支出 ⑤収支 
                    ⑥金融資産1 ⑦金融資産2 ⑧資産 
                    ⑨残業時間 ⑩残業代 ⑪ローン 
                    ⑫電気量 ⑬電気 ⑭ガス量 ⑮ガス 
                    ⑯水道量 ⑰水道 ⑱積立 
                    ⑲臨時収入メモ1 ⑳臨時収入1 
                    ㉑臨時収入メモ2 ㉒臨時収入2 
                    ㉓臨時収入メモ3 ㉔臨時収入3 
                    ㉕臨時収入メモ4 ㉖臨時収入4 
                    ㉗出費メモ1 ㉘出費1 
                    ㉙出費メモ2 ㉚出費2 
                    ㉛出費メモ3 ㉜出費3 
                    ㉝出費メモ4 ㉞出費4 
                    ㉟メモ ㊱メモ編集(年メモ)。
                </p>

                <h2>3. 各項目とアプリ内データ</h2>
                <p>
                    所得・総支給・支出・資産・残業時間・残業代・ローン・積立は、
                    通常月の対応する入力値を出力します。
                    収支は「所得 − 支出 − ローン − 積立」で計算します。
                    臨時収入・出費はメモと金額を各4組の固定列に分けて出力します。
                    金融資産1・2は既存の資産とは別の項目で、
                    通常月・賞与それぞれの保存値を出力します。
                </p>

                <h2>4. 電気・ガス・水道の量と金額</h2>
                <p>
                    「電気量」「ガス量」「水道量」は使用量、「電気」「ガス」「水道」は料金です。
                    量と金額は別々の列に保存され、互いに合算・変換されません。
                    読取り時も、それぞれの見出しに対応するアプリ内項目へ反映します。
                </p>

                <h2>5. 年メモと通常月・賞与のメモ</h2>
                <p>
                    「メモ」は各レコードのメモです。
                    「メモ編集」は年別画面で編集する年メモです。
                    年メモは、その年で最も古い年月のレコードの行にだけ出力します。
                    最も古いレコードが賞与でも、その賞与行に年メモを出力します。
                    読取りでは通常月・賞与のどちらの行からも年メモを取得し、
                    同じ年に異なる年メモがあれば不一致としてエラーになります。
                </p>
                <p>
                    賞与の行は年月の末尾に「賞与」を付けて区別します。
                    所得・総支給・メモ・金融資産1・金融資産2を出力し、
                    それ以外の項目は空欄です。賞与は通常月とは別に読み取られます。
                </p>

                <h2>6. 読取り時のチェックと既存データ</h2>
                <p>
                    最初のシートを読み取り、見出し名に基づいて項目を対応付けます。
                    必須の「年月」がない場合、年月形式や数値が不正な場合、
                    同じ年月の通常月または賞与が重複する場合はエラーになります。
                    年月が空欄の行は読み飛ばします。
                    対応しない見出しは無視し、重複した見出しは誤読防止のため無視します。
                </p>
                <p>
                    ODSの値が空欄の項目は、原則として既存データを空欄で
                    上書きしない方式です。既存データとの重複時は確認画面が表示されます。
                    読取り途中で問題が発生した場合は、エラー内容を確認してください。
                    臨時収入・出費はそれぞれ最大4件で、5件目以降の明細列があるODSは
                    読取りを中止します。
                </p>

                <h2>7. 読取り・出力の注意点</h2>
                <p>
                    ・見出し名と列の対応を維持してください。列の順番を変えても
                    見出しが正しければ対応付けできますが、見出しの変更・重複には注意してください。<br>
                    ・空欄は「0」と同じ意味ではありません。空欄項目は既存値の保持に関係します。<br>
                    ・収支は出力時に計算されます。臨時収入・出費を収支へ自動加算しません。<br>
                    ・年メモは各行に繰り返し出力されません。年ごとの最古のレコード行を確認してください。<br>
                    ・読取り前に既存データをバックアップし、読取り後は年別画面・一覧画面で値を確認してください。
                </p>
            </div>

            <div class="form-actions">
                <button class="secondary-btn" onclick="showPage('settings')">
                    設定へ戻る
                </button>
            </div>
        </div>
    `;
}

/* =========================================================
   ODS出力
========================================================= */

function exportOds() {
    if (typeof XLSX === "undefined") {
        alert(
            "ODS出力ライブラリを読み込めません。\n" +
            "インターネット接続とConsoleを確認してください。"
        );
        return;
    }

    try {
        const headers = [
    "年月",
    "所得",
    "総支給",
    "支出",
    "収支",
    "金融資産1",
    "金融資産2",
    "資産",
    "残業時間",
    "残業代",
    "ローン",
    "電気量",
    "電気",
    "ガス量",
    "ガス",
    "水道量",
    "水道",
    "積立",
    "臨時収入メモ1",
    "臨時収入1",
    "臨時収入メモ2",
    "臨時収入2",
    "臨時収入メモ3",
    "臨時収入3",
    "臨時収入メモ4",
    "臨時収入4",
    "出費メモ1",
    "出費1",
    "出費メモ2",
    "出費2",
    "出費メモ3",
    "出費3",
    "出費メモ4",
    "出費4",
    "メモ",
    "メモ編集"
];

        const rows = [headers];

        // 通常月のデータを作成
        const records = Object.keys(data.months || {})
            .map(yearMonth => ({
                yearMonth,
                type: "month",
                value: data.months[yearMonth]
            }))
            .filter(record => record.value);

        // 賞与を通常月とは別の行として追加
        (data.bonuses || []).forEach(bonus => {
            if (!bonus || !bonus.yearMonth) return;

            records.push({
                yearMonth: bonus.yearMonth,
                type: "bonus",
                value: bonus
            });
        });

        // 新しい年月を上にする。
        // 同じ年月なら通常月を賞与より先にする。
        records.sort((a, b) => {
            const dateCompare =
                String(b.yearMonth).localeCompare(
                    String(a.yearMonth)
                );

            if (dateCompare !== 0) return dateCompare;

            if (a.type === b.type) return 0;

            return a.type === "month" ? -1 : 1;
        });

        // 年ごとに最も古い年月の行を特定する。
        // 同じ年月なら通常月を優先する。
        const oldestRecordByYear = {};

        records.forEach(record => {
            const year = String(record.yearMonth).substring(0, 4);
            const current = oldestRecordByYear[year];

            if (
                !current ||
                record.yearMonth < current.yearMonth ||
                (
                    record.yearMonth === current.yearMonth &&
                    record.type === "month" &&
                    current.type === "bonus"
                )
            ) {
                oldestRecordByYear[year] = record;
            }
        });

        records.forEach(record => {
            const yearMonth = String(record.yearMonth);
            const year = yearMonth.substring(0, 4);
            const value = record.value;

            // 賞与は指定された5項目を出力する。
            // 年内で最も古いレコードが賞与の場合は年メモも出力する。
            if (record.type === "bonus") {
                const row = Array(headers.length).fill("");

                row[0] = yearMonth + "賞与";
                row[1] = value.income ?? "";
                row[2] = value.grossPay ?? "";
                row[5] = value.financialAsset1 ?? "";
                row[6] = value.financialAsset2 ?? "";
row[34] = value.memo ?? "";
                row[35] =
                    oldestRecordByYear[year] === record
                        ? (data.yearMemos?.[year] || "")
                        : "";

                rows.push(row);
                return;
            }

            const month = value;

            const temporaryIncome =
                Array.isArray(month.temporaryIncome)
                    ? month.temporaryIncome
                    : [];

            const expenses =
                Array.isArray(month.expenses)
                    ? month.expenses
                    : [];

            const income = Number(month.income || 0);
            const expense = Number(month.expense || 0);
            const loan = Number(month.loan || 0);
            const saving = Number(month.saving || 0);

            // 基本収支の既存仕様を維持
            const balance =
                income - expense - loan - saving;

            const row = [
                yearMonth,
                income,
                Number(month.grossPay || 0),
                expense,
                balance,

                // 金融資産欄は、保存値が存在する場合だけ出力
                month.financialAsset1 ?? "",
                month.financialAsset2 ?? "",

                Number(month.asset || 0),
                Number(month.overtimeHours || 0),
                Number(month.overtimePay || 0),
                loan,

                Number(month.electricity?.kwh || 0),
                Number(month.electricity?.amount || 0),

                Number(month.gas?.volume || 0),
                Number(month.gas?.amount || 0),

                Number(month.water?.volume || 0),
                Number(month.water?.amount || 0),

                saving,

                // 臨時収入4組を先に出力し、その後に出費4組を出力
                temporaryIncome[0]?.memo ?? "",
                temporaryIncome[0]?.amount ?? "",
                temporaryIncome[1]?.memo ?? "",
                temporaryIncome[1]?.amount ?? "",
                temporaryIncome[2]?.memo ?? "",
                temporaryIncome[2]?.amount ?? "",
                temporaryIncome[3]?.memo ?? "",
                temporaryIncome[3]?.amount ?? "",

                expenses[0]?.memo ?? "",
                expenses[0]?.amount ?? "",
                expenses[1]?.memo ?? "",
                expenses[1]?.amount ?? "",
                expenses[2]?.memo ?? "",
                expenses[2]?.amount ?? "",
                expenses[3]?.memo ?? "",
                expenses[3]?.amount ?? "",

                String(month.memo || ""),

                // 年メモは年内で最も古い年月の行だけに出力
                oldestRecordByYear[year] === record
                    ? (data.yearMemos?.[year] || "")
                    : ""
            ];

            rows.push(row);
        });

        const worksheet = XLSX.utils.aoa_to_sheet(rows);
        const workbook = XLSX.utils.book_new();

        XLSX.utils.book_append_sheet(
            workbook,
            worksheet,
            "支出管理"
        );

        XLSX.writeFile(
            workbook,
            "expenditure-backup.ods",
            { bookType: "ods" }
        );

    } catch (error) {
        console.error("ODS出力に失敗しました。", error);

        alert(
            "ODS出力に失敗しました。\n" +
            "F12のConsoleに表示されたエラーを確認してください。"
        );
    }
}

/* =========================================================
   ODS読み取り
========================================================= */

async function importOds() {
    if (typeof XLSX === "undefined") {
        alert("ODSライブラリが読み込まれていません。");
        return;
    }

    const input = document.createElement("input");
    input.type = "file";
    input.accept = ".ods";

    input.onchange = async () => {
        const file = input.files?.[0];
        if (!file) return;

        try {
            const workbook = XLSX.read(
                await file.arrayBuffer(),
                { type: "array" }
            );

            const sheetName = workbook.SheetNames[0];
            if (!sheetName) {
                throw new Error("シートがありません。");
            }

            const rows = XLSX.utils.sheet_to_json(
                workbook.Sheets[sheetName],
                { header: 1, defval: "" }
            );

            if (rows.length < 2) {
                throw new Error("読み取れるデータがありません。");
            }

            const headers = rows[0].map(
                value => String(value ?? "").trim()
            );

// 5件目以降の明細があるODSは、切り捨てずに読み込みを中止する。
            const excessDetailHeaders = headers.filter(header => {
                const match = header.match(
                    /^(?:臨時収入|出費)(?:メモ)?(\d+)$/
                );

                return match && Number(match[1]) > 4;
            });

            if (excessDetailHeaders.length > 0) {
                throw new Error(
                    "ODSに5件目以降の明細列があります。\n" +
                    "明細は臨時収入・出費とも最大4件です。\n" +
                    "超過列: " +
                    excessDetailHeaders.join("、") +
                    "\n\nデータは読み込んでいません。"
                );
            }

            // アプリが対応する項目だけを認識する。
            // 対応しない見出しは無視する。
            const aliases = {
                "年月": "yearMonth",
                "所得": "income",
                "総支給": "grossPay",
                "支出": "expense",
                "金融資産1": "financialAsset1",
                "金融資産2": "financialAsset2",
                "資産": "asset",
                "残業時間": "overtimeHours",
                "残業代": "overtimePay",
                "ローン": "loan",
                "電気": "electricity.amount",
                "電気量": "electricity.kwh",
                "kwh": "electricity.kwh",
                "ガス": "gas.amount",
                "ガス量": "gas.volume",
                "水道": "water.amount",
                "水道量": "water.volume",
                "積立": "saving",
"臨時収入メモ1": "temporaryIncomeMemo1",
"臨時収入1": "temporaryIncomeAmount1",
"臨時収入メモ2": "temporaryIncomeMemo2",
"臨時収入2": "temporaryIncomeAmount2",
"臨時収入メモ3": "temporaryIncomeMemo3",
"臨時収入3": "temporaryIncomeAmount3",
"臨時収入メモ4": "temporaryIncomeMemo4",
"臨時収入4": "temporaryIncomeAmount4",

"出費メモ1": "expenseMemo1",
"出費1": "expenseAmount1",
"出費メモ2": "expenseMemo2",
"出費2": "expenseAmount2",
"出費メモ3": "expenseMemo3",
"出費3": "expenseAmount3",
"出費メモ4": "expenseMemo4",
"出費4": "expenseAmount4",
                "メモ": "memo",
                "メモ編集": "yearMemo"
            };

            const columns = {};
            const counts = {};

            headers.forEach((header, index) => {
                if (aliases[header]) {
                    counts[header] = (counts[header] || 0) + 1;

                    if (counts[header] === 1) {
                        columns[header] = index;
                    }
                }
            });

            // 重複した見出しは誤った列を選ばないよう無視する。
            Object.keys(counts).forEach(header => {
                if (counts[header] > 1) {
                    delete columns[header];
                    console.warn(
                        "重複見出しを無視しました:",
                        header
                    );
                }
            });

            if (columns["年月"] === undefined) {
                throw new Error(
                    "必須項目「年月」が見つかりません。"
                );
            }

            const cell = (row, header) => {
                const index = columns[header];

                if (index === undefined) return "";

                return row[index] ?? "";
            };

            const isBlank = value =>
                value === "" ||
                value === null ||
                value === undefined ||
                String(value).trim() === "";

            function readNumber(value, label, line) {
                if (isBlank(value)) return null;

                const number = Number(value);

                if (!Number.isFinite(number)) {
                    throw new Error(
                        line + "行目の「" + label +
                        "」が数値ではありません。"
                    );
                }

                return number;
            }

            function parseYearMonth(value, line) {
                let text = String(value ?? "").trim();
                let isBonus = false;

                if (text.endsWith("賞与")) {
                    isBonus = true;
                    text = text.slice(0, -2).trim();
                }

                const match = text.match(
                    /^(\d{4})[.\/-](\d{1,2})$/
                );

                if (!match) {
                    throw new Error(
                        line + "行目の年月が不正です: " + value
                    );
                }

                const month = Number(match[2]);

                if (month < 1 || month > 12) {
                    throw new Error(
                        line + "行目の月が不正です。"
                    );
                }

                return {
                    yearMonth:
                        match[1] + "-" +
                        String(month).padStart(2, "0"),
                    isBonus
                };
            }

            function parseDetails(value, line, label) {
                if (isBlank(value)) return null;

                return String(value).split(" / ").map(entry => {
                    const separator = entry.lastIndexOf(":");

                    if (separator < 0) {
                        throw new Error(
                            line + "行目の「" + label +
                            "」の明細形式が不正です。"
                        );
                    }

                    const memo = entry.slice(0, separator);
                    const amount = Number(
                        entry.slice(separator + 1)
                    );

                    if (!Number.isFinite(amount)) {
                        throw new Error(
                            line + "行目の「" + label +
                            "」の金額が不正です。"
                        );
                    }

                    return { memo, amount };
                });
            }

            const importedMonths = {};
            const importedBonuses = {};
            const importedYearMemos = {};

            rows.slice(1).forEach((row, index) => {
                if (
                    !row ||
                    row.every(value => isBlank(value))
                ) {
                    return;
                }

                const line = index + 2;

                // 年月が空欄の行は読み飛ばす
                const rawYearMonth = cell(row, "年月");

                if (isBlank(rawYearMonth)) {
                    console.warn(
                        line + "行目は年月が空欄のため読み飛ばしました。"
                    );
                    return;
                }

                const date = parseYearMonth(
                    rawYearMonth,
                    line
                );

                const key = date.yearMonth;
                const year = key.slice(0, 4);

// 通常月・賞与のどちらの行でも年メモを取得する。
                const yearMemo = cell(row, "メモ編集");

                if (!isBlank(yearMemo)) {
                    if (
                        importedYearMemos[year] !== undefined &&
                        importedYearMemos[year] !== String(yearMemo)
                    ) {
                        throw new Error(
                            year + "年の年メモが一致しません。"
                        );
                    }

                    importedYearMemos[year] = String(yearMemo);
                }

                if (date.isBonus) {
                    if (importedBonuses[key]) {
                        throw new Error(
                            "ODS内に同じ年月の賞与が複数あります: " +
                            key
                        );
                    }

                    const bonus = {};

                    const income = readNumber(
                        cell(row, "所得"), "所得", line
                    );
                    const grossPay = readNumber(
                        cell(row, "総支給"), "総支給", line
                    );
 const financialAsset1 = readNumber(
                        cell(row, "金融資産1"),
                        "金融資産1",
                        line
                    );

                    const financialAsset2 = readNumber(
                        cell(row, "金融資産2"),
                        "金融資産2",
                        line
                    );

                    if (income !== null) bonus.income = income;
                    if (grossPay !== null) bonus.grossPay = grossPay;
                    if (financialAsset1 !== null) bonus.financialAsset1 = financialAsset1;
                    if (financialAsset2 !== null) bonus.financialAsset2 = financialAsset2;

const bonusMemo = cell(row, "メモ");

if (!isBlank(bonusMemo)) {
    bonus.memo = String(bonusMemo);
}

                    importedBonuses[key] = bonus;
                    return;
                }

                if (importedMonths[key]) {
                    throw new Error(
                        "ODS内に同じ年月の通常月が複数あります: " +
                        key
                    );
                }

                // 既存データがあれば複製し、
                // ODSに値がある項目だけを更新する。
                const existing = data.months?.[key];

                const month = existing
                    ? JSON.parse(JSON.stringify(existing))
                    : {
                        yearMonth: key,
                        income: 0,
                        grossPay: 0,
                        expense: 0,
                        temporaryIncome: [],
                        expenses: [],
                        asset: 0,
                        overtimePay: 0,
                        overtimeHours: 0,
                        loan: 0,
                        electricity: { amount: 0, kwh: 0 },
                        gas: { amount: 0, volume: 0 },
                        water: { amount: 0, volume: 0 },
                        saving: 0,
                        memo: ""
                    };

                month.yearMonth = key;

                const numericFields = [
                    ["所得", "income"],
                    ["総支給", "grossPay"],
                    ["支出", "expense"],
                    ["金融資産1", "financialAsset1"],
                    ["金融資産2", "financialAsset2"],
                    ["資産", "asset"],
                    ["残業時間", "overtimeHours"],
                    ["残業代", "overtimePay"],
                    ["ローン", "loan"],
                    ["積立", "saving"],
                   
                ];

                const appliedFields = new Set();

                numericFields.forEach(([header, property]) => {
                    if (appliedFields.has(property)) return;

                    const value = readNumber(
                        cell(row, header), header, line
                    );

                    if (value !== null) {
                        month[property] = value;
                        appliedFields.add(property);
                    }
                });

                const nestedFields = [
                    ["電気", "electricity", "amount"],
                    ["電気量", "electricity", "kwh"],
                    ["kwh", "electricity", "kwh"],
                    ["ガス", "gas", "amount"],
                    ["ガス量", "gas", "volume"],
                    ["水道", "water", "amount"],
                    ["水道量", "water", "volume"]
                ];

                const appliedNested = new Set();

                nestedFields.forEach(([header, group, property]) => {
                    const id = group + "." + property;
                    if (appliedNested.has(id)) return;

                    const value = readNumber(
                        cell(row, header), header, line
                    );

                    if (value !== null) {
                        month[group] = {
                            ...(month[group] || {}),
                            [property]: value
                        };

                        appliedNested.add(id);
                    }
                });

                const memo = cell(row, "メモ");

                if (!isBlank(memo)) {
                    month.memo = String(memo);
                }

// 36項目の固定列から臨時収入・出費を復元する。
function readDetailRows(prefix, line) {
    const details = [];

    for (let i = 1; i <= 4; i++) {
        const memoHeader = prefix + "メモ" + i;
        const amountHeader = prefix + i;

        const memoValue = cell(row, memoHeader);
        const amountValue = cell(row, amountHeader);

        const memoBlank = isBlank(memoValue);
        const amountBlank = isBlank(amountValue);

        // メモと金額の両方が空欄なら、この明細は読み飛ばす。
        if (memoBlank && amountBlank) {
            continue;
        }

        // メモだけ、または金額だけの入力も保持する。
        const memo = memoBlank
            ? ""
            : String(memoValue);

        let amount = 0;

        if (!amountBlank) {
            amount = readNumber(
                amountValue,
                amountHeader,
                line
            );

            if (amount === null) {
                amount = 0;
            }
        }

        details.push({
            memo,
            amount
        });
    }

    return details;
}

// ODSに対応する見出しがある場合だけ明細を更新する。
// 既存データにない列は、既存の明細を維持する。
const hasIncomeDetailColumns = [
    "臨時収入メモ1",
    "臨時収入1",
    "臨時収入メモ2",
    "臨時収入2",
    "臨時収入メモ3",
    "臨時収入3",
    "臨時収入メモ4",
    "臨時収入4"
].some(header => columns[header] !== undefined);

const hasExpenseDetailColumns = [
    "出費メモ1",
    "出費1",
    "出費メモ2",
    "出費2",
    "出費メモ3",
    "出費3",
    "出費メモ4",
    "出費4"
].some(header => columns[header] !== undefined);

if (hasIncomeDetailColumns) {
                    const details = readDetailRows(
                        "臨時収入",
                        line
                    );

                    if (details.length > 0) {
                        month.temporaryIncome = details;
                    }
                }

                if (hasExpenseDetailColumns) {
                    const details = readDetailRows(
                        "出費",
                        line
                    );

                    if (details.length > 0) {
                        month.expenses = details;
                    }
                }

                importedMonths[key] = month;

            });

            const monthKeys = Object.keys(importedMonths);
            const bonusKeys = Object.keys(importedBonuses);

            if (
                monthKeys.length === 0 &&
                bonusKeys.length === 0
            ) {
                throw new Error("有効な月データがありません。");
            }

            const overlaps = [];

            monthKeys.forEach(key => {
                if (data.months?.[key]) {
                    overlaps.push(key + "(通常月)");
                }
            });

            bonusKeys.forEach(key => {
                if (
                    (data.bonuses || []).some(
                        item => item.yearMonth === key
                    )
                ) {
                    overlaps.push(key + "(賞与)");
                }
            });

            Object.keys(importedYearMemos).forEach(year => {
                if (
                    Object.prototype.hasOwnProperty.call(
                        data.yearMemos || {}, year
                    )
                ) {
                    overlaps.push(year + "年の年メモ");
                }
            });

            let message =
                "通常月 " + monthKeys.length +
                "件、賞与 " + bonusKeys.length +
                "件を読み込みます。\n";

            if (overlaps.length) {
                message +=
                    "\n既存データと重複しています。\n" +
                    overlaps.join("\n") +
                    "\n\n続行すると該当項目を上書きします。";
            }

            if (!confirm(message + "\n\n続行しますか?")) {
                return;
            }

            const previousMonths = data.months;
            const previousBonuses = data.bonuses || [];
            const previousYearMemos = data.yearMemos || {};

            const nextMonths = {
                ...previousMonths,
                ...importedMonths
            };

            const nextBonuses = previousBonuses.map(
                item => {
                    const imported = importedBonuses[item.yearMonth];

                    if (!imported) return item;

                    return { ...item, ...imported };
                }
            );

            bonusKeys.forEach(key => {
                const exists = nextBonuses.some(
                    item => item.yearMonth === key
                );

                if (!exists) {
                    nextBonuses.push({
                        id: Date.now() + nextBonuses.length,
                        yearMonth: key,
                        income: importedBonuses[key].income ?? 0,
                        grossPay: importedBonuses[key].grossPay ?? 0,
                       financialAsset1:
                            importedBonuses[key].financialAsset1 ?? 0,
                        financialAsset2:
                            importedBonuses[key].financialAsset2 ?? 0,
memo: importedBonuses[key].memo ?? ""
                    });
                }
            });

            const nextYearMemos = {
                ...previousYearMemos,
                ...importedYearMemos
            };

            data.months = nextMonths;
            data.bonuses = nextBonuses;
            data.yearMemos = nextYearMemos;

            try {
                await saveData();
            } catch (error) {
                data.months = previousMonths;
                data.bonuses = previousBonuses;
                data.yearMemos = previousYearMemos;
                throw error;
            }

            alert(
                "ODS読取り・保存が完了しました。\n" +
                "通常月: " + monthKeys.length + "件\n" +
                "賞与: " + bonusKeys.length + "件"
            );

            showPage("years");

        } catch (error) {
            console.error("ODS読み取りに失敗しました。", error);

            alert(
                "ODS読み取りに失敗しました。\n\n" +
                error.message
            );
        }
    };

    input.click();
}

function fontSettingRow(
        type,
        label,
        value
    ) {
        const descriptions = {
            heading: "アプリ名,上部バー",
            year: "年別画面",
            month: "月別画面",
            add: "追加・賞与編集",
            setting: "設定画面,背景画像",
            description: "アプリ説明,バックアップ画面"
        };

        const sizeVariables = {
            heading: "--heading-size",
            year: "--year-size",
            month: "--month-size",
            add: "--add-size",
            setting: "--setting-size",
            description: "--description-size"
        };

        const sizeVariable =
            sizeVariables[type] || "--setting-size";

        return `
            <div class="setting-row">

                <div
                    style="
                        font-size: var(--setting-size);
                        display: flex;
                        align-items: baseline;
                        flex-wrap: wrap;
                        gap: 4px 10px;
                    "
                >
                    <span>${label}</span>

                    <span
                        style="
                            font-size: calc(var(--setting-size) * 0.75);
                            font-weight: 400;
                            color: var(--sub-text);
                        "
                    >
                        ${descriptions[type] || ""}
                    </span>
                </div>

                <div class="setting-control">

                    <input
                        id="font-${type}"
                        type="range"
                        min="10"
                        max="40"
                        value="${value}"
                    >

                    <span
                        id="font-${type}-value"
                        class="range-value"
                    >
                        ${value}px
                    </span>

                    <select
                        id="font-family-${type}"
                        aria-label="${label}のフォント"
                    >
                        <option
    value="yu-gothic"
    style="font-family: 'Yu Gothic', YuGothic, sans-serif;"
>游ゴシック</option>

<option
    value="meiryo"
    style="font-family: Meiryo, sans-serif;"
>メイリオ</option>

<option
    value="biz-udp-gothic"
    style="font-family: 'BIZ UDPGothic', 'BIZ UDPゴシック', sans-serif;"
>BIZ UDPゴシック</option>

<option
    value="biz-udp-mincho"
    style="font-family: 'BIZ UDPMincho', 'BIZ UDP明朝', serif;"
>BIZ UDP明朝</option>

<option
    value="ms-p-gothic"
    style="font-family: 'MS PGothic', 'MS Pゴシック', sans-serif;"
>MS Pゴシック</option>

<option
    value="ms-p-mincho"
    style="font-family: 'MS PMincho', 'MS P明朝', serif;"
>MS P明朝</option>
                    </select>

                </div>

            </div>
        `;
    }


async function saveSettingPage() {

    settings.darkMode =
        document
            .getElementById(
                "darkModeSetting"
            )
            .checked;


    settings.mainColor =
        document
            .getElementById(
                "mainColorSetting"
            )
            .value;


settings.appTitle =
document
.getElementById(
"appTitleSetting"
)
.value
.trim() || "支出管理";


    settings.fontSizes.heading =
        Number(
            document
                .getElementById(
                    "font-heading"
                )
                .value
        );


    settings.fontSizes.year =
    Number(
        document
            .getElementById(
                "font-year"
            )
            .value
    );

settings.fontSizes.month =
    Number(
        document
            .getElementById(
                "font-month"
            )
            .value
    );

settings.fontSizes.add =
    Number(
        document
            .getElementById(
                "font-add"
            )
            .value
    );

    settings.fontSizes.setting =
        Number(
            document
                .getElementById(
                    "font-setting"
                )
                .value
        );


    settings.fontSizes.description =
        Number(
            document
                .getElementById(
                    "font-description"
                )
                .value
        );


       await saveSettings();

    applySettings();

}
    

function applySettings() {


const appTitle =
settings.appTitle || "支出管理";

document.title = appTitle;

const appTitleElement =
document.getElementById("appTitle");

if (appTitleElement) {
appTitleElement.textContent =
appTitle;
}

const startupTitle =
document.querySelector(".startup-title");

if (startupTitle) {
startupTitle.textContent =
appTitle;
}

    document.documentElement.style
        .setProperty(
            "--main-color",
            settings.mainColor
        );


    document.documentElement.style
        .setProperty(
            "--heading-size",
            settings.fontSizes.heading +
            "px"
        );


    document.documentElement.style
        .setProperty(
            "--year-size",
            settings.fontSizes.year +
            "px"
        );

document.documentElement.style
    .setProperty(
        "--month-size",
        settings.fontSizes.month +
        "px"
    );

    document.documentElement.style
        .setProperty(
            "--add-size",
            settings.fontSizes.add +
            "px"
        );


    document.documentElement.style
        .setProperty(
            "--setting-size",
            settings.fontSizes.setting +
            "px"
        );


    document.documentElement.style
        .setProperty(
            "--description-size",
            settings.fontSizes.description +
            "px"
        );


    document.body.classList.toggle(
        "dark",
        settings.darkMode
    );


    document.documentElement.style
        .setProperty(
            "--main-color-light",
            hexToRgba(
                settings.mainColor,
                .12
            )
        );


    applyBackground();

}


/* =========================================================
   背景画像
========================================================= */

function renderBackgroundPage() {
document
.getElementById("main")
.innerHTML = `

<div class="setting-section">
<h3>
背景画像
</h3>

<p class="setting-message">
画像を指定すると、
指定フォルダに保存します。
</p>

<input
id="backgroundImageInput"
type="file"
accept="image/*"
>

<div
style="margin-top:20px;"
>
<button
class="primary-btn"
onclick="saveBackgroundImage()"
>
背景画像を設定
</button>

<button
class="secondary-btn"
onclick="removeBackgroundImage()"
>
背景画像を消去
</button>
</div>
</div>

<div class="setting-section">
<h3>
背景カラー
</h3>

<div class="setting-row">
<div>
<div>
背景カラー
</div>
<div class="setting-message">
背景画像が設定されていない場合のみ有効です。
</div>
</div>

<div class="setting-control">
<input
id="backgroundColorSetting"
type="color"
value="${settings.backgroundColor}"
${settings.backgroundImageName ? "disabled" : ""}
>
</div>
</div>

${
settings.backgroundImageName
? `
<div class="setting-message">
背景画像が設定されているため、
背景カラーは現在無効です。
</div>
`
: ""
}
</div>

<div class="setting-section">
<h3>
背景透過
</h3>

<div class="setting-row">
<div>
<div>
背景透過
</div>
<div class="setting-message">
背景画像が設定されている場合のみ有効です。
</div>
</div>

<div class="setting-control">
<input
id="backgroundOpacity"
    type="range"
    min="0"
max="100"
step="10"
value="${Math.round((1 - Number(settings.backgroundOpacity ?? 1)) * 100)}"
    ${settings.backgroundImageName ? "" : "disabled"}
    style="width:180px;"
>

<span
id="backgroundOpacityValue"
class="range-value"
>
${Math.min(100, Math.max(0, Math.round((1 - Number(settings.backgroundOpacity ?? 1)) * 10) * 10))}%
</span>
</div>
</div>

${
!settings.backgroundImageName
? `
<div class="setting-message">
背景画像が設定されていないため、
背景透過は現在無効です。
</div>
`
: ""
}
</div>

<div class="form-actions">
<button
class="primary-btn"
onclick="saveBackgroundSettings()"
>
背景設定を保存
</button>
</div>
`;

const opacityInput =
document.getElementById(
"backgroundOpacity"
);

if (opacityInput) {
opacityInput.addEventListener(
"input",
e => {
document
.getElementById(
"backgroundOpacityValue"
)
.textContent =
Number(e.target.value) + "%"
}
);
}
}

async function saveBackgroundSettings() {

const colorInput =
document.getElementById(
"backgroundColorSetting"
);

const opacityInput =
document.getElementById(
"backgroundOpacity"
);

if (
colorInput &&
!colorInput.disabled
) {
settings.backgroundColor =
colorInput.value;
}

if (
opacityInput &&
!opacityInput.disabled
) {
settings.backgroundOpacity =
    1 - Number(opacityInput.value) / 100;
}

await saveSettings();

applySettings();

renderBackgroundPage();

}

async function saveBackgroundImage() {

    const input =
        document.getElementById(
            "backgroundImageInput"
        );


    if (!input.files.length) {

        alert(
            "画像を選択してください。"
        );

        return;
    }


    const file =
        input.files[0];


    const handle =
        await directoryHandle
            .getFileHandle(
                "background-image",
                {
                    create: true
                }
            );


    const writable =
        await handle.createWritable();


    await writable.write(
        file
    );


    await writable.close();


    settings.backgroundImageName =
        "background-image";


    settings.backgroundOpacity =
    1 - Number(
        document.getElementById(
            "backgroundOpacity"
        ).value
    ) / 100;

await saveSettings();
await applyBackground();

renderBackgroundPage();

}


async function removeBackgroundImage() {

    try {

        await directoryHandle
            .removeEntry(
                "background-image"
            );

    } catch (error) {
        /* 存在しない場合は無視 */
    }


    settings.backgroundImageName =
"";

await saveSettings();

document.body.style
.backgroundImage = "";

applySettings();

renderBackgroundPage();

}


async function applyBackground() {
    const root = document.documentElement;

    if (
        !directoryHandle ||
        !settings.backgroundImageName
    ) {
        document.body.style.backgroundImage = "";
        document.body.style.backgroundColor =
            settings.backgroundColor;

        root.style.setProperty(
                "--background-image",
                "none"
            );
            root.style.removeProperty("--card-opacity");

            return;
    }

    try {
        const handle = await directoryHandle.getFileHandle(
            settings.backgroundImageName,
            { create: false }
        );

        const file = await handle.getFile();
        const url = URL.createObjectURL(file);

        document.body.style.backgroundImage = "";
        document.body.style.backgroundColor =
            settings.backgroundColor;

        root.style.setProperty(
            "--background-image",
            `url("${url}")`
        );

        // 透過するのはカード・枠の背景色だけ
            const cardOpacity = Math.min(
    1,
    Math.max(
        0,
        Number(settings.backgroundOpacity ?? 1)
    )
);

        root.style.setProperty(
            "--card-opacity",
            String(cardOpacity)
        );

    } catch (error) {
        console.error(
            "背景画像の読み込みに失敗しました。",
            error
        );

        root.style.setProperty(
            "--background-image",
            "none"
        );
        root.style.removeProperty("--card-opacity");

        document.body.style.backgroundImage = "";
        document.body.style.backgroundColor =
            settings.backgroundColor;
    }
}


/* =========================================================
   初期化
========================================================= */

async function resetRecords() {

    const first =
        confirm(
            "警告\n\n" +
            "記録を初期化します。\n" +
            "年別データ、月別データ、年メモがすべて削除されます。\n\n" +
            "この操作は元に戻せません。\n\n" +
            "本当に初期化しますか?"
        );

    if (!first) {
        return;
    }

    const second =
        confirm(
            "最終確認\n\n" +
            "保存フォルダ内の記録データと一時保存データを削除します。\n" +
            "実行してよろしいですか?"
        );

    if (!second) {
        return;
    }

    try {

        /*
         * 正式データをファイルごと削除
         */
        await deleteJsonFile(
            DATA_FILE
        );

        /*
         * 一時保存データもファイルごと削除
         */
        await deleteJsonFile(
            DRAFT_FILE
        );

        /*
         * アプリ内のデータも初期化
         */
        data = {
            months: {},
            bonuses: [],
            yearMemos: {}
        };

        editingMonthKey = null;

        /*
         * 年別画面を表示
         */
        renderYears();

        
    } catch (error) {

        console.error(
            "記録初期化に失敗しました。",
            error
        );

        alert(
            "記録の初期化に失敗しました。\n\n" +
            error.message
        );
    }
}

/* =========================================================
   一覧ページ
========================================================= */

async function saveListSettings() {
    try {
        await saveSettings();
    } catch (error) {
        console.error(
            "一覧設定の保存に失敗しました。",
            error
        );

        alert(
            "一覧設定の保存に失敗しました。\n\n" +
            error.message
        );
    }
}

function showListMemo(button) {
    const existing = document.getElementById("listMemoPopup");

    // 開いているメモをクリックした場合は閉じる
    if (
        existing &&
        existing.dataset.sourceButton === button.dataset.memoId
    ) {
        existing.remove();
        return;
    }

    // 別のメモが開いていれば閉じる
    if (existing) {
        existing.remove();
    }

    const popup = document.createElement("div");
    popup.id = "listMemoPopup";
    popup.dataset.sourceButton = button.dataset.memoId || "";

    Object.assign(popup.style, {
        position: "fixed",
        zIndex: "99999",
        left: "50%",
        top: "50%",
        transform: "translate(-50%, -50%)",
        width: "min(420px, calc(100vw - 32px))",
        maxHeight: "70vh",
        overflowY: "auto",
        padding: "18px",
        boxSizing: "border-box",
        border: "1px solid var(--border-color)",
        borderRadius: "10px",
        background: "var(--card-color)",
        color: "var(--text-color)",
        boxShadow: "0 4px 20px rgba(0,0,0,0.25)",
        whiteSpace: "pre-wrap",
        overflowWrap: "anywhere"
    });

    const title = document.createElement("div");
    title.textContent = "明細メモ";
    title.style.fontWeight = "bold";
    title.style.marginBottom = "12px";

    const content = document.createElement("div");
    content.textContent = button.dataset.memo || "";

    popup.append(title, content);
    document.body.appendChild(popup);

    // メモの外側をクリックすると閉じる
    const closeOnOutsideClick = event => {
        if (
            !popup.contains(event.target) &&
            !event.target.closest(".list-memo-button")
        ) {
            popup.remove();
            document.removeEventListener(
                "click",
                closeOnOutsideClick,
                true
            );
        }
    };

    // 表示直後のクリックで閉じないよう、次の操作から判定
    setTimeout(() => {
        if (popup.isConnected) {
            document.addEventListener(
                "click",
                closeOnOutsideClick,
                true
            );
        }
    }, 0);
}

async function changeListSize(size) {
    settings.listSize = size;

    await saveListSettings();

    renderListPage();
}

async function toggleListYear(year, checked) {
    const selected = new Set(
        Array.isArray(settings.listYears)
            ? settings.listYears.map(String)
            : []
    );

    if (checked) {
        selected.add(String(year));
    } else {
        selected.delete(String(year));
    }

    settings.listYears = [...selected].sort(
        (a, b) => Number(b) - Number(a)
    );

    await saveListSettings();

    renderListPage(true);
}

async function clearListYears() {
    settings.listYears = [];

    await saveListSettings();

    // 現在の年月パネルの展開状態を維持する
    const panel = document.getElementById("listYearPanel");
    const isExpanded = panel
        ? !panel.classList.contains("hidden")
        : false;

    renderListPage(isExpanded);
}

function renderListPage(keepYearPanel = false) {
    const main = document.getElementById("main");

    const size = Math.min(
        5,
        Math.max(1, Number(settings.listSize ?? 4))
    );

    const selectedYears = Array.isArray(settings.listYears)
        ? settings.listYears.map(String)
        : [];

    const years = getYears().map(String);

    const normalRows = Object.entries(data.months || {})
        .map(([key, month]) => ({
            type: "normal",
            yearMonth: month.yearMonth || key,
            data: month
        }));

    const bonusRows = (data.bonuses || [])
        .map(bonus => ({
            type: "bonus",
            yearMonth: bonus.yearMonth,
            data: bonus
        }));

    const rows = [
        ...normalRows,
        ...bonusRows
    ]
        .filter(row => {
            if (!row.yearMonth) {
                return false;
            }

            if (selectedYears.length === 0) {
                return true;
            }

            return selectedYears.includes(
                String(row.yearMonth).substring(0, 4)
            );
        })
        .sort((a, b) => {
            const dateCompare =
                String(b.yearMonth).localeCompare(
                    String(a.yearMonth)
                );

            if (dateCompare !== 0) {
                return dateCompare;
            }

            // 同じ年月は通常月を先、賞与を後にする
            if (a.type !== b.type) {
                return a.type === "normal" ? -1 : 1;
            }

            return 0;
        });

    const headers = [
        "年月",
        "所得",
        "総支給",
        "支出",
        "収支",
        "金融1",
        "金融2",
        "資産",
        "残業H",
        "残業代",
        "ローン",
        "電気量",
        "電気",
        "ガス量",
        "ガス",
        "水道量",
        "水道",
        "積立",
        "メモ",
        "臨時1",
        "収入1",
        "臨時2",
        "収入2",
        "臨時3",
        "収入3",
        "臨時4",
        "収入4",
        "出費m",
        "出費1",
        "出費m2",
        "出費2",
        "出費m3",
        "出費3",
        "出費m4",
        "出費4"
    ];

    const moneyCell = value =>
        money(Number(value || 0));

    const detailMemoCell = memo => {
        if (!String(memo || "").trim()) {
            return '<td class="list-detail-memo">-</td>';
        }

        return `
            <td class="list-detail-memo">
                <button
                    type="button"
                    class="list-memo-button"
                    data-memo="${escapeHtml(memo)}"
                    onclick="showListMemo(this)"
                    title="メモを別ウィンドウで表示"
                >📝</button>
            </td>
        `;
    };

    const normalCell = value =>
        `<td>${escapeHtml(value)}</td>`;

    const numericCell = value =>
        `<td>${moneyCell(value)}</td>`;

    const rowsHtml = rows.map(row => {
        const d = row.data;

        if (row.type === "bonus") {
            const redCell = value =>
                `<td class="list-bonus">${escapeHtml(value)}</td>`;

            const redMoney = value =>
                `<td class="list-bonus">${moneyCell(value)}</td>`;

            const dash = "<td>-</td>";

            return `
                <tr>
                    <td class="list-bonus">
    <span
        class="year-link"
        onclick="editBonus('${escapeHtml(d.id)}')"
    >${escapeHtml(row.yearMonth)}</span>
</td>
                    ${redMoney(d.income)}
                    ${redMoney(d.grossPay)}
                    ${dash}
                    ${dash}
                    ${redMoney(d.financialAsset1)}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${dash}
                    ${
    String(d.memo || "").trim()
        ? `<td class="list-memo-cell list-detail-memo list-bonus">
                <button
                    type="button"
                    class="list-memo-button"
                    data-memo-id="list-bonus-memo-${escapeHtml(d.id)}"
                    data-memo="${escapeHtml(d.memo)}"
                    onclick="showListMemo(this)"
                    title="賞与のメモを表示"
                >📃</button>
           </td>`
        : '<td class="list-memo-cell list-detail-memo list-bonus">-</td>'
}
                    ${Array.from({ length: 16 }, () => dash).join("")}
                </tr>
            `;
        }

        const income = Number(d.income || 0);
        const expense = Number(d.expense || 0);
        const loan = Number(d.loan || 0);
        const saving = Number(d.saving || 0);

        // 既存仕様:収支 = 所得 - 支出 - ローン - 積立
        const balance = income - expense - loan - saving;

        const temporaryIncome = Array.isArray(d.temporaryIncome)
            ? d.temporaryIncome
            : [];

        const expenses = Array.isArray(d.expenses)
            ? d.expenses
            : [];

        const electricity = d.electricity || {};
        const gas = d.gas || {};
        const water = d.water || {};

        const incomeDetails = [];
        const expenseDetails = [];

        for (let i = 0; i < 4; i++) {
            const item = temporaryIncome[i] || {};

            incomeDetails.push(
                detailMemoCell(item.memo),
                item.memo || Number(item.amount || 0) !== 0
                    ? numericCell(item.amount)
                    : '<td>-</td>'
            );
        }

        for (let i = 0; i < 4; i++) {
            const item = expenses[i] || {};

            expenseDetails.push(
                detailMemoCell(item.memo),
                item.memo || Number(item.amount || 0) !== 0
                    ? numericCell(item.amount)
                    : '<td>-</td>'
            );
        }

        return `
            <tr>
                <td>
                    <span
                        class="year-link"
                        onclick="editMonth('${escapeHtml(row.yearMonth)}')"
                    >${escapeHtml(row.yearMonth)}</span>
                </td>
                ${numericCell(d.income)}
                ${numericCell(d.grossPay)}
                ${numericCell(d.expense)}
                ${numericCell(balance)}
                ${numericCell(d.financialAsset1)}
                ${numericCell(d.financialAsset2)}
                ${numericCell(d.asset)}
                ${normalCell(d.overtimeHours ?? 0)}
                ${numericCell(d.overtimePay)}
                ${numericCell(d.loan)}
                ${normalCell(electricity.kwh ?? 0)}
                ${numericCell(electricity.amount)}
                ${normalCell(gas.volume ?? 0)}
                ${numericCell(gas.amount)}
                ${normalCell(water.volume ?? 0)}
                ${numericCell(water.amount)}
                ${numericCell(d.saving)}
                <td class="list-memo-cell list-detail-memo">
    ${
        String(d.memo || "").trim()
            ? `<button
                    type="button"
                    class="list-memo-button"
                    data-memo-id="list-month-memo-${escapeHtml(row.yearMonth)}"
                    data-memo="${escapeHtml(d.memo)}"
                    onclick="showListMemo(this)"
                    title="メモを表示"
               >📃</button>`
            : "-"
    }
</td>
                ${incomeDetails.join("")}
                ${expenseDetails.join("")}
            </tr>
        `;
    }).join("");

    main.innerHTML = `

        <div class="list-toolbar">
            <button
                type="button"
                id="listYearToggle"
                class="${keepYearPanel ? "active" : ""}"
            >年月 ▼</button>
<button
    type="button"
    id="listYearClear"
>解除</button>

            <span>サイズ</span>

            ${[1, 2, 3, 4, 5].map(n => `
                <button
                    type="button"
                    class="${size === n ? "active" : ""}"
                    data-list-size="${n}"
                >${n}</button>
            `).join("")}

            <span>
                ${rows.length}件
            </span>
        </div>

        <div
            id="listYearPanel"
            class="list-year-panel ${keepYearPanel ? "" : "hidden"}"
        >
            ${years.map(year => `
                <label>
                    <input
                        type="checkbox"
                        data-list-year="${year}"
                        ${selectedYears.includes(year) ? "checked" : ""}
                    >
                    ${year}
                </label>
            `).join("")}

        </div>

        <div class="list-table-wrapper list-size-${size}">
            <table class="list-table">
                <thead>
                    <tr>
                        ${headers.map(h => `<th>${h}</th>`).join("")}
                    </tr>
                </thead>
                <tbody>
                    ${rowsHtml || `
                        <tr>
                            <td colspan="${headers.length}"
                                class="list-empty">
                                該当する記録がありません。
                            </td>
                        </tr>
                    `}
                </tbody>
            </table>
        </div>
    `;

    document.getElementById("listYearToggle")
    .addEventListener("click", () => {
        const panel = document.getElementById("listYearPanel");
        const button = document.getElementById("listYearToggle");

        panel.classList.toggle("hidden");

        const isExpanded = !panel.classList.contains("hidden");

        button.textContent = isExpanded
            ? "年月 △"
            : "年月 ▽";

        button.classList.toggle("active", isExpanded);
        });

    document.querySelectorAll("[data-list-size]")
        .forEach(button => {
            button.addEventListener("click", () => {
                changeListSize(Number(button.dataset.listSize));
            });
        });

    document.querySelectorAll("[data-list-year]")
        .forEach(checkbox => {
            checkbox.addEventListener("change", () => {
                toggleListYear(
                    checkbox.dataset.listYear,
                    checkbox.checked
                );
            });
        });

    document.getElementById("listYearClear")
        .addEventListener("click", clearListYears);
}

/* =========================================================
   HTMLエスケープ
========================================================= */

function escapeHtml(value) {

    return String(
        value ?? ""
    )

    .replace(
        /&/g,
        "&amp;"
    )

    .replace(
        /</g,
        "&lt;"
    )

    .replace(
        />/g,
        "&gt;"
    )

    .replace(
        /"/g,
        "&quot;"
    )

    .replace(
        /'/g,
        "&#039;"
    );

}


/* =========================================================
   色
========================================================= */

function hexToRgba(
    hex,
    alpha
) {

    hex =
        hex.replace(
            "#",
            ""
        );


    if (hex.length === 3) {

        hex =
            hex[0] + hex[0] +
            hex[1] + hex[1] +
            hex[2] + hex[2];

    }


    const r =
        parseInt(
            hex.substring(0,2),
            16
        );


    const g =
        parseInt(
            hex.substring(2,4),
            16
        );


    const b =
        parseInt(
            hex.substring(4,6),
            16
        );


    return `
        rgba(
            ${r},
            ${g},
            ${b},
            ${alpha}
        )
    `;

}


/* =========================================================
   グローバル公開
========================================================= */

window.showPage =
    showPage;

window.showYearDetail =
    showYearDetail;

window.openYearMemo =
    openYearMemo;

window.saveYearMemo =
    saveYearMemo;

window.closeModal =
    closeModal;

window.addEditorLine =
    addEditorLine;

window.saveMonthForm =
    saveMonthForm;

window.editMonth =
    editMonth;

window.saveSettingPage =
    saveSettingPage;

window.saveBackgroundImage =
    saveBackgroundImage;

window.removeBackgroundImage =
    removeBackgroundImage;

window.resetRecords =
    resetRecords;

</script>

</body>
</html>

185,208 chars · codepastes.com