3126 lines
105 KiB
HTML
3126 lines
105 KiB
HTML
<!doctype html>
|
||
<html lang="ja">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<script src="js/auth.js"></script>
|
||
<title>仕訳入力</title>
|
||
<style>
|
||
body {
|
||
font-family: sans-serif;
|
||
margin: 24px;
|
||
}
|
||
h2 {
|
||
margin: 0 0 12px;
|
||
}
|
||
table {
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
margin-top: 12px;
|
||
}
|
||
th,
|
||
td {
|
||
border: 1px solid #ccc;
|
||
padding: 6px;
|
||
text-align: center;
|
||
}
|
||
th {
|
||
background: #f5f5f5;
|
||
}
|
||
input,
|
||
select,
|
||
button {
|
||
padding: 6px;
|
||
}
|
||
.right {
|
||
text-align: right;
|
||
}
|
||
.row {
|
||
margin: 8px 0;
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: center;
|
||
}
|
||
.row label {
|
||
min-width: 120px;
|
||
}
|
||
.search-form {
|
||
background: #f9f9f9;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 6px;
|
||
padding: 20px;
|
||
margin: 16px 0;
|
||
}
|
||
.search-form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 20px 30px;
|
||
margin-bottom: 16px;
|
||
}
|
||
.search-form-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
.search-form-row label {
|
||
min-width: 100px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
.search-form-row input,
|
||
.search-form-row select {
|
||
flex: 1;
|
||
padding: 8px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
.search-form-row input:focus,
|
||
.search-form-row select:focus {
|
||
outline: none;
|
||
border-color: #4caf50;
|
||
box-shadow: 0 0 4px rgba(76, 175, 80, 0.3);
|
||
}
|
||
.search-form-row-full {
|
||
grid-column: 1 / -1;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
}
|
||
.search-buttons {
|
||
display: flex;
|
||
gap: 8px;
|
||
justify-content: flex-start;
|
||
margin-top: 8px;
|
||
}
|
||
.search-buttons button {
|
||
padding: 8px 20px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
}
|
||
.search-buttons button:first-child {
|
||
background: #4caf50;
|
||
color: white;
|
||
}
|
||
.search-buttons button:first-child:hover {
|
||
background: #45a049;
|
||
}
|
||
.search-buttons button:last-child {
|
||
background: white;
|
||
color: #2196f3;
|
||
border: 1px solid #2196f3;
|
||
}
|
||
.search-buttons button:last-child:hover {
|
||
background: #f0f8ff;
|
||
}
|
||
.tax-row {
|
||
background: #f0f8ff;
|
||
}
|
||
.print-header {
|
||
display: none;
|
||
}
|
||
.page-break {
|
||
page-break-before: always;
|
||
}
|
||
.page-number {
|
||
display: none;
|
||
}
|
||
@media print {
|
||
@page {
|
||
margin: 5mm 15mm 20mm 15mm;
|
||
@bottom-center {
|
||
content: "ページ " counter(page);
|
||
font-size: 0.75em;
|
||
}
|
||
}
|
||
body * {
|
||
visibility: hidden;
|
||
}
|
||
#printArea,
|
||
#printArea * {
|
||
visibility: visible;
|
||
}
|
||
#printArea {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 100%;
|
||
}
|
||
button,
|
||
.no-print {
|
||
display: none !important;
|
||
}
|
||
.print-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 8px;
|
||
font-size: 0.9em;
|
||
padding-bottom: 4px;
|
||
}
|
||
.print-header-left {
|
||
text-align: left;
|
||
}
|
||
.print-header-right {
|
||
text-align: right;
|
||
}
|
||
.page-number {
|
||
display: none !important;
|
||
}
|
||
#printArea table {
|
||
font-size: 0.85em;
|
||
}
|
||
#printArea th,
|
||
#printArea td {
|
||
padding: 3px 4px;
|
||
}
|
||
#printArea h3 {
|
||
font-size: 1.1em;
|
||
margin-bottom: 6px;
|
||
}
|
||
thead {
|
||
display: table-header-group;
|
||
}
|
||
}
|
||
|
||
/* 账户搜索下拉框相关样式 */
|
||
.account-search-container {
|
||
position: relative;
|
||
width: 100%;
|
||
}
|
||
|
||
.account-search-input {
|
||
width: 100%;
|
||
padding: 6px !important;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.account-search-input:focus {
|
||
outline: none;
|
||
border-color: #4caf50;
|
||
box-shadow: 0 0 4px rgba(76, 175, 80, 0.3);
|
||
}
|
||
|
||
.account-dropdown {
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
right: 0;
|
||
background: white;
|
||
border: 1px solid #ccc;
|
||
border-top: none;
|
||
border-radius: 0 0 4px 4px;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
z-index: 1000;
|
||
display: none;
|
||
}
|
||
|
||
.account-dropdown.active {
|
||
display: block;
|
||
}
|
||
|
||
.account-option {
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
border-bottom: 1px solid #f0f0f0;
|
||
font-size: 14px;
|
||
text-align: left;
|
||
display: block;
|
||
width: 100%;
|
||
}
|
||
|
||
.account-option:hover,
|
||
.account-option.selected {
|
||
background-color: #e3f2fd;
|
||
}
|
||
|
||
.account-option-strong {
|
||
font-weight: bold;
|
||
color: #333;
|
||
display: inline;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.account-option-light {
|
||
color: #555;
|
||
font-size: 14px;
|
||
display: inline;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<button
|
||
onclick="location.href = 'index.html'"
|
||
class="back-button"
|
||
style="
|
||
margin: 0 0 20px 0;
|
||
padding: 8px 16px;
|
||
background: #007bff;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
"
|
||
>
|
||
← 首ページに戻る
|
||
</button>
|
||
<h2>仕訳入力</h2>
|
||
|
||
<div class="row" style="margin-bottom: 12px">
|
||
<label style="min-width: 80px">区分</label>
|
||
<label
|
||
style="
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
"
|
||
>
|
||
<input
|
||
type="radio"
|
||
name="entryType"
|
||
value="expense"
|
||
checked
|
||
style="transform: scale(1.3)"
|
||
/>
|
||
【支出】
|
||
</label>
|
||
<label
|
||
style="
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
font-size: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
margin-left: 16px;
|
||
"
|
||
>
|
||
<input
|
||
type="radio"
|
||
name="entryType"
|
||
value="income"
|
||
style="transform: scale(1.3)"
|
||
/>
|
||
【収入】
|
||
</label>
|
||
</div>
|
||
|
||
<div
|
||
style="
|
||
margin: -4px 0 12px 96px;
|
||
font-size: 13px;
|
||
color: #666;
|
||
line-height: 1.8;
|
||
"
|
||
>
|
||
<span style="margin-right: 24px">
|
||
【支出】 借方:<b>科目</b> / 貸方:<b>取引手段</b>
|
||
</span>
|
||
<span> 【収入】 借方:<b>取引手段</b> / 貸方:<b>科目</b> </span>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<label>仕訳日</label>
|
||
<input
|
||
type="date"
|
||
id="entryDate"
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
onchange="onEntryDateChanged()"
|
||
/>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<label>摘要</label>
|
||
<div
|
||
class="account-search-container"
|
||
style="flex: 3; min-width: 600px; max-width: 900px; position: relative"
|
||
>
|
||
<input
|
||
type="text"
|
||
id="description"
|
||
class="account-search-input"
|
||
style="width: 100%"
|
||
placeholder="例:売上入金/経費支払 など"
|
||
autocomplete="off"
|
||
/>
|
||
<div class="account-dropdown" id="descriptionDropdown"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<label>科目</label>
|
||
<div class="account-search-container" style="flex: 2; max-width: 400px">
|
||
<input
|
||
type="text"
|
||
id="entryAccountInput"
|
||
class="account-search-input"
|
||
placeholder="科目を入力・検索"
|
||
autocomplete="off"
|
||
/>
|
||
<div class="account-dropdown"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<label>取引手段</label>
|
||
<div class="account-search-container" style="flex: 2; max-width: 400px">
|
||
<input
|
||
type="text"
|
||
id="entryMethodInput"
|
||
class="account-search-input"
|
||
placeholder="取引手段を入力・検索(例:現金、普通預金)"
|
||
autocomplete="off"
|
||
/>
|
||
<div class="account-dropdown"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<label>金額</label>
|
||
<input
|
||
type="text"
|
||
id="entryAmount"
|
||
class="right"
|
||
inputmode="numeric"
|
||
style="ime-mode: inactive; width: 200px"
|
||
placeholder="税込金額"
|
||
oninput="
|
||
formatNumberInput(this);
|
||
calcTax();
|
||
"
|
||
/>
|
||
<span style="margin-left: -8px">円</span>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<label>消費税率</label>
|
||
<select id="entryTaxRate" style="width: 160px" onchange="calcTax()">
|
||
<option value="0">対象外</option>
|
||
<option value="10" selected>10%</option>
|
||
<option value="8">8%</option>
|
||
<option value="8r">8%(軽減)</option>
|
||
<option value="5">5%</option>
|
||
</select>
|
||
<label style="margin-left: 16px; white-space: nowrap"
|
||
>小数点以下端数処理</label
|
||
>
|
||
<select id="entryRoundingMode" style="width: 120px" onchange="calcTax()">
|
||
<option value="floor" selected>切り捨て</option>
|
||
<option value="round">四捨五入</option>
|
||
<option value="ceil">切り上げ</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="row">
|
||
<label>うち消費税額</label>
|
||
<input
|
||
type="text"
|
||
id="entryTaxAmount"
|
||
class="right"
|
||
inputmode="numeric"
|
||
style="ime-mode: inactive; width: 200px"
|
||
placeholder="自動計算(手動変更可)"
|
||
oninput="formatNumberInput(this)"
|
||
/>
|
||
<span style="margin-left: -8px">円</span>
|
||
</div>
|
||
|
||
<div style="margin-top: 14px; display: flex; gap: 8px; align-items: center">
|
||
<button
|
||
onclick="submitJournal()"
|
||
style="
|
||
padding: 8px 24px;
|
||
font-size: 15px;
|
||
background: #28a745;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
"
|
||
>
|
||
登録
|
||
</button>
|
||
<label
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
margin-left: 20px;
|
||
cursor: pointer;
|
||
"
|
||
>
|
||
<input type="checkbox" id="keepDetailsAfterSubmit" />
|
||
<span style="font-size: 14px">登録後に明細を保持する</span>
|
||
</label>
|
||
</div>
|
||
|
||
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
|
||
|
||
<!-- ========================================
|
||
仕訳詳細入力(多行対応)
|
||
======================================== -->
|
||
<details
|
||
style="margin: 16px 0"
|
||
open
|
||
id="detailSection"
|
||
ontoggle="
|
||
document.getElementById('detailSummaryText').textContent = this.open
|
||
? '▼ 仕訳詳細入力(複数行・借貸直接入力)'
|
||
: '▶ 仕訳詳細入力(複数行・借貸直接入力)'
|
||
"
|
||
>
|
||
<summary
|
||
style="
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
padding: 12px;
|
||
background: #eaf0fb;
|
||
border: 1px solid #c8d8f0;
|
||
border-radius: 4px;
|
||
user-select: none;
|
||
list-style: none;
|
||
"
|
||
>
|
||
<span id="detailSummaryText"
|
||
>▼ 仕訳詳細入力(複数行・借貸直接入力)</span
|
||
>
|
||
</summary>
|
||
|
||
<div
|
||
style="
|
||
background: #f9fbff;
|
||
border: 1px solid #c8d8f0;
|
||
border-top: none;
|
||
border-radius: 0 0 8px 8px;
|
||
padding: 20px;
|
||
margin-bottom: 16px;
|
||
"
|
||
>
|
||
<!-- 仕訳日 -->
|
||
<div class="row" style="margin-bottom: 10px">
|
||
<label style="min-width: 80px">仕訳日</label>
|
||
<input
|
||
type="date"
|
||
id="detailEntryDate"
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
style="padding: 6px"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 摘要 -->
|
||
<div class="row" style="margin-bottom: 10px">
|
||
<label style="min-width: 80px">摘要</label>
|
||
<div
|
||
class="account-search-container"
|
||
style="
|
||
flex: 3;
|
||
min-width: 600px;
|
||
max-width: 900px;
|
||
position: relative;
|
||
"
|
||
>
|
||
<input
|
||
type="text"
|
||
id="detailDescription"
|
||
class="account-search-input"
|
||
style="width: 100%"
|
||
placeholder="例:売上入金/経費支払 など"
|
||
autocomplete="off"
|
||
/>
|
||
<div class="account-dropdown" id="detailDescriptionDropdown"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 明細テーブル -->
|
||
<table
|
||
id="detailLinesTable"
|
||
style="border-collapse: collapse; width: 100%; margin-top: 8px"
|
||
>
|
||
<thead>
|
||
<tr style="background: #eaf0fb">
|
||
<th
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 10px;
|
||
width: 24%;
|
||
text-align: center;
|
||
"
|
||
>
|
||
科目
|
||
</th>
|
||
<th
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 10px;
|
||
width: 22%;
|
||
text-align: center;
|
||
"
|
||
>
|
||
借方金額
|
||
</th>
|
||
<th
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 10px;
|
||
width: 22%;
|
||
text-align: center;
|
||
"
|
||
>
|
||
貸方金額
|
||
</th>
|
||
<th
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 10px;
|
||
width: 14%;
|
||
text-align: center;
|
||
"
|
||
>
|
||
摘要(行)
|
||
</th>
|
||
<th
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 4px;
|
||
width: 6%;
|
||
text-align: center;
|
||
"
|
||
></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="detailLinesTbody"></tbody>
|
||
<tfoot>
|
||
<tr style="background: #f5f5f5; font-weight: bold">
|
||
<td
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 10px;
|
||
text-align: center;
|
||
"
|
||
>
|
||
合計
|
||
</td>
|
||
<td
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 10px;
|
||
text-align: right;
|
||
"
|
||
id="detailDebitTotal"
|
||
>
|
||
0
|
||
</td>
|
||
<td
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 10px;
|
||
text-align: right;
|
||
"
|
||
id="detailCreditTotal"
|
||
>
|
||
0
|
||
</td>
|
||
<td
|
||
colspan="2"
|
||
style="
|
||
border: 1px solid #ccc;
|
||
padding: 7px 10px;
|
||
text-align: center;
|
||
"
|
||
>
|
||
<span id="detailBalanceStatus" style="font-size: 13px"></span>
|
||
</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
|
||
<!-- ボタン類 -->
|
||
<div
|
||
style="
|
||
margin-top: 12px;
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
"
|
||
>
|
||
<button
|
||
onclick="detailAddLine()"
|
||
style="
|
||
padding: 7px 18px;
|
||
background: #6c757d;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
"
|
||
>
|
||
+ 行追加
|
||
</button>
|
||
<button
|
||
onclick="submitDetailJournal()"
|
||
style="
|
||
padding: 8px 28px;
|
||
font-size: 15px;
|
||
background: #28a745;
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
"
|
||
>
|
||
登録
|
||
</button>
|
||
<label
|
||
style="
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
margin-left: 8px;
|
||
"
|
||
>
|
||
<input type="checkbox" id="detailKeepAfterSubmit" />
|
||
<span style="font-size: 14px">登録後に明細を保持する</span>
|
||
</label>
|
||
<button
|
||
onclick="detailClearAll()"
|
||
style="
|
||
padding: 7px 14px;
|
||
background: #fff;
|
||
color: #dc3545;
|
||
border: 1px solid #dc3545;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
margin-left: auto;
|
||
"
|
||
>
|
||
🗑 クリア
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
|
||
|
||
<!-- 月次锁定管理(折叠式) -->
|
||
<details style="margin: 16px 0">
|
||
<summary
|
||
style="
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
font-weight: bold;
|
||
padding: 12px;
|
||
background: #f5f5f5;
|
||
border-radius: 4px;
|
||
user-select: none;
|
||
"
|
||
>
|
||
▶ 月次锁定管理
|
||
</summary>
|
||
|
||
<div
|
||
style="
|
||
padding: 16px;
|
||
background: #fafafa;
|
||
border-radius: 4px;
|
||
margin-top: 8px;
|
||
"
|
||
>
|
||
<div style="margin-bottom: 8px">
|
||
<label>年度:</label>
|
||
<input
|
||
type="number"
|
||
id="lockYear"
|
||
value="2025"
|
||
min="1900"
|
||
max="2999"
|
||
style="width: 80px"
|
||
/>
|
||
|
||
<label>月份:</label>
|
||
<input
|
||
type="number"
|
||
id="lockMonth"
|
||
min="1"
|
||
max="12"
|
||
value="6"
|
||
style="width: 60px"
|
||
/>
|
||
</div>
|
||
|
||
<button onclick="lockMonth()">🔒 锁定</button>
|
||
<button onclick="unlockMonth()">🔓 解锁</button>
|
||
|
||
<div id="lockResult" style="margin-top: 10px; color: #333"></div>
|
||
|
||
<div style="margin-top: 12px">
|
||
<button onclick="refreshMonthLocks()">🔄 刷新锁定列表</button>
|
||
<pre id="lockList" style="background: #f7f7f7; padding: 8px"></pre>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<div
|
||
id="monthLockBanner"
|
||
style="
|
||
display: none;
|
||
margin: 8px 0;
|
||
padding: 6px 10px;
|
||
background: #ffecec;
|
||
border: 1px solid #ffaaaa;
|
||
color: #a00000;
|
||
"
|
||
>
|
||
🔒 该月份已锁定,不能编辑仕訳
|
||
</div>
|
||
|
||
<!-- ========================================
|
||
仕訳検索セクション(journal-list.htmlから統合)
|
||
======================================== -->
|
||
<hr style="margin: 40px 0; border: none; border-top: 2px solid #ddd" />
|
||
|
||
<h2>仕訳検索</h2>
|
||
|
||
<div class="search-form">
|
||
<div class="search-form-grid">
|
||
<div class="search-form-row">
|
||
<label>期間</label>
|
||
<div style="display: flex; gap: 8px; align-items: center; flex: 1">
|
||
<input
|
||
type="date"
|
||
id="searchFromDate"
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
<span style="color: #999">~</span>
|
||
<input
|
||
type="date"
|
||
id="searchToDate"
|
||
min="1900-01-01"
|
||
max="2099-12-31"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="search-form-row">
|
||
<label>科目</label>
|
||
<div class="account-search-container">
|
||
<input
|
||
type="text"
|
||
id="searchAccountInput"
|
||
class="account-search-input"
|
||
placeholder="科目を入力・検索"
|
||
autocomplete="off"
|
||
/>
|
||
<div class="account-dropdown"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="search-form-row" style="grid-column: 1 / -1">
|
||
<label>摘要</label>
|
||
<input type="text" id="searchKeyword" />
|
||
</div>
|
||
|
||
<div class="search-form-row">
|
||
<label>方向</label>
|
||
<select id="searchAccountSide">
|
||
<option value="">-- すべて --</option>
|
||
<option value="debit">借方</option>
|
||
<option value="credit">貸方</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="search-form-row">
|
||
<label style="min-width: auto">
|
||
<input
|
||
type="checkbox"
|
||
id="searchIncludeHistory"
|
||
style="margin-right: 6px"
|
||
/>
|
||
修正履歴を含む
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="search-buttons">
|
||
<button onclick="searchJournals()">🔍 検索</button>
|
||
<button onclick="printSearchResults()">🖨️ 印刷</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="printArea"></div>
|
||
|
||
<script>
|
||
const API = "";
|
||
|
||
// カスタム通知トースト
|
||
function showAlert(msg, type) {
|
||
const configs = {
|
||
success: {
|
||
icon: "✔",
|
||
iconColor: "#22a84b",
|
||
bg: "#f0faf4",
|
||
border: "#22a84b",
|
||
},
|
||
error: {
|
||
icon: "✖",
|
||
iconColor: "#e53935",
|
||
bg: "#fff5f5",
|
||
border: "#e53935",
|
||
},
|
||
warning: {
|
||
icon: "⚠",
|
||
iconColor: "#e67e00",
|
||
bg: "#fffbf0",
|
||
border: "#f0a500",
|
||
},
|
||
};
|
||
const c = configs[type] || configs.warning;
|
||
const container = document.getElementById("alertToastContainer");
|
||
const toast = document.createElement("div");
|
||
toast.style.cssText = `pointer-events:auto;display:flex;align-items:flex-start;gap:12px;background:${c.bg};border:1px solid ${c.border};border-left:5px solid ${c.border};border-radius:8px;padding:14px 16px;min-width:280px;max-width:420px;box-shadow:0 4px 16px rgba(0,0,0,0.14);animation:alertSlideIn 0.25s ease;`;
|
||
const msgEsc = String(msg).replace(/&/g, "&").replace(/</g, "<");
|
||
toast.innerHTML = `<span style="font-size:26px;color:${c.iconColor};line-height:1;flex-shrink:0;">${c.icon}</span><span style="font-size:14px;color:#333;flex:1;white-space:pre-wrap;margin-top:3px;">${msgEsc}</span><button onclick="this.parentElement.remove()" style="background:none;border:none;cursor:pointer;color:#aaa;font-size:20px;line-height:1;padding:0 0 0 8px;flex-shrink:0;">×</button>`;
|
||
container.appendChild(toast);
|
||
setTimeout(() => {
|
||
if (toast.parentElement) toast.remove();
|
||
}, 4500);
|
||
}
|
||
|
||
// 千分位カンマフォーマット関数
|
||
function formatNumberInput(input) {
|
||
let value = input.value.replace(/,/g, ""); // 既存のカンマを削除
|
||
if (value === "" || isNaN(value)) {
|
||
input.value = "";
|
||
return;
|
||
}
|
||
// 数値をカンマ区切りに変換
|
||
input.value = Number(value).toLocaleString();
|
||
}
|
||
|
||
let accounts = [];
|
||
|
||
const ACCOUNT_GROUPS = [
|
||
// 資産の部
|
||
{
|
||
label: "現金・預金",
|
||
codes: ["101", "102", "103", "104", "105"],
|
||
},
|
||
{
|
||
label: "売上債権",
|
||
codes: ["201", "202"],
|
||
},
|
||
{
|
||
label: "有価証券",
|
||
codes: ["301"],
|
||
},
|
||
{
|
||
label: "棚卸資産",
|
||
codes: ["210"],
|
||
},
|
||
{
|
||
label: "他流動資産",
|
||
codes: ["203", "204", "205", "206", "207", "208"],
|
||
},
|
||
{
|
||
label: "有形固定資産",
|
||
codes: ["401", "402", "403", "404", "405", "406", "407"],
|
||
},
|
||
{
|
||
label: "無形固定資産",
|
||
codes: ["408"],
|
||
},
|
||
{
|
||
label: "投資その他資産",
|
||
codes: ["410"],
|
||
},
|
||
// 負債の部
|
||
{
|
||
label: "仕入債務",
|
||
codes: ["501"],
|
||
},
|
||
{
|
||
label: "その他流動負債",
|
||
codes: ["502", "503", "504", "505", "506", "507", "508"],
|
||
},
|
||
{
|
||
label: "固定負債",
|
||
codes: ["509"],
|
||
},
|
||
// 資本の部
|
||
{
|
||
label: "資本金",
|
||
codes: ["601"],
|
||
},
|
||
{
|
||
label: "利益剰余金",
|
||
codes: ["602"],
|
||
},
|
||
// 収益・費用
|
||
{
|
||
label: "収益",
|
||
codes: ["701", "702", "703", "704", "705"],
|
||
},
|
||
{
|
||
label: "費用",
|
||
codes: [
|
||
"801",
|
||
"802",
|
||
"803",
|
||
"804",
|
||
"805",
|
||
"806",
|
||
"807",
|
||
"808",
|
||
"809",
|
||
"810",
|
||
"811",
|
||
"812",
|
||
"813",
|
||
"814",
|
||
"815",
|
||
"816",
|
||
"817",
|
||
"818",
|
||
"819",
|
||
"820",
|
||
],
|
||
},
|
||
];
|
||
|
||
let taxPaidAccounts = [];
|
||
let taxReceivedAccounts = [];
|
||
let rowSeq = 0; // 行ID計数器
|
||
|
||
// ========================================
|
||
// (旧 setupAccountSearch は setupEntryAccountSearch に統合済)
|
||
// ========================================
|
||
|
||
// ========================================
|
||
// 消費税額 自動計算
|
||
// ========================================
|
||
function calcTax() {
|
||
const amountStr = document
|
||
.getElementById("entryAmount")
|
||
.value.replace(/,/g, "");
|
||
const amount = Number(amountStr) || 0;
|
||
const rateVal = document.getElementById("entryTaxRate").value;
|
||
const roundingMode =
|
||
document.getElementById("entryRoundingMode")?.value || "floor";
|
||
|
||
if (amount === 0 || rateVal === "0") {
|
||
document.getElementById("entryTaxAmount").value = "";
|
||
return;
|
||
}
|
||
|
||
// 税率を数値に変換(8r = 軽減8%)
|
||
const rate = rateVal === "8r" ? 8 : Number(rateVal);
|
||
const rawTax = (amount * rate) / (100 + rate);
|
||
|
||
// 端数処理
|
||
const roundFn =
|
||
roundingMode === "ceil"
|
||
? Math.ceil
|
||
: roundingMode === "round"
|
||
? Math.round
|
||
: Math.floor;
|
||
const taxAmount = roundFn(rawTax);
|
||
|
||
document.getElementById("entryTaxAmount").value =
|
||
taxAmount.toLocaleString();
|
||
}
|
||
|
||
// ========================================
|
||
// 入力フォーム用科目検索セットアップ
|
||
// ========================================
|
||
function setupEntryAccountSearch(inputElement) {
|
||
const container = inputElement.closest(".account-search-container");
|
||
const dropdown = container.querySelector(".account-dropdown");
|
||
|
||
function appendAccountOption(acc, onSelect) {
|
||
const option = document.createElement("div");
|
||
option.className = "account-option";
|
||
option.innerHTML = `<span class="account-option-strong">${acc.account_code}</span><span class="account-option-light">${acc.account_name}</span>`;
|
||
option.addEventListener("mousedown", (ev) => {
|
||
ev.preventDefault();
|
||
inputElement.value = `${acc.account_code} ${acc.account_name}`;
|
||
inputElement.dataset.accountId = acc.account_id;
|
||
dropdown.classList.remove("active");
|
||
saveAccountToRecent(acc.account_id);
|
||
if (onSelect) onSelect();
|
||
});
|
||
dropdown.appendChild(option);
|
||
}
|
||
|
||
function makeSectionLabel(text) {
|
||
const lbl = document.createElement("div");
|
||
lbl.style.cssText =
|
||
"padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
|
||
lbl.textContent = text;
|
||
return lbl;
|
||
}
|
||
|
||
function renderDropdown(list, isFiltered) {
|
||
dropdown.innerHTML = "";
|
||
if (list.length === 0) {
|
||
dropdown.innerHTML =
|
||
'<div style="padding: 8px 12px; color: #999;">該当する科目がありません</div>';
|
||
dropdown.classList.add("active");
|
||
return;
|
||
}
|
||
|
||
if (isFiltered) {
|
||
// 検索中 → コード順で表示
|
||
const sorted = list
|
||
.slice()
|
||
.sort((a, b) => a.account_code.localeCompare(b.account_code));
|
||
sorted.slice(0, 50).forEach((acc) => appendAccountOption(acc));
|
||
} else {
|
||
// 未入力 → 最近使った科目を先頭に、残りはコード順
|
||
const recentIds = JSON.parse(
|
||
localStorage.getItem("recentAccounts") || "[]",
|
||
);
|
||
const recentItems = recentIds
|
||
.map((id) => list.find((a) => a.account_id === id))
|
||
.filter(Boolean);
|
||
const recentSet = new Set(recentIds);
|
||
const otherItems = list
|
||
.filter((a) => !recentSet.has(a.account_id))
|
||
.sort((a, b) => a.account_code.localeCompare(b.account_code));
|
||
|
||
if (recentItems.length > 0) {
|
||
dropdown.appendChild(makeSectionLabel("⭐ 最近使った科目"));
|
||
recentItems.forEach((acc) => appendAccountOption(acc));
|
||
const sep = document.createElement("div");
|
||
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
|
||
dropdown.appendChild(sep);
|
||
}
|
||
dropdown.appendChild(makeSectionLabel("📋 すべての科目"));
|
||
otherItems.slice(0, 50).forEach((acc) => appendAccountOption(acc));
|
||
}
|
||
|
||
dropdown.classList.add("active");
|
||
}
|
||
|
||
inputElement.addEventListener("input", (e) => {
|
||
const searchText = e.target.value.toLowerCase().trim();
|
||
inputElement.dataset.accountId = "";
|
||
|
||
if (!searchText) {
|
||
renderDropdown(accounts, false);
|
||
return;
|
||
}
|
||
|
||
const filtered = accounts.filter((acc) => {
|
||
const code = acc.account_code.toLowerCase();
|
||
const name = acc.account_name.toLowerCase();
|
||
return (
|
||
code.includes(searchText) ||
|
||
name.includes(searchText) ||
|
||
`${code} ${name}`.includes(searchText)
|
||
);
|
||
});
|
||
|
||
renderDropdown(filtered, true);
|
||
});
|
||
|
||
document.addEventListener("click", (e) => {
|
||
if (!container.contains(e.target)) {
|
||
dropdown.classList.remove("active");
|
||
}
|
||
});
|
||
|
||
inputElement.addEventListener("focus", () => {
|
||
inputElement.dataset.accountId = inputElement.dataset.accountId || "";
|
||
const searchText = inputElement.value.toLowerCase().trim();
|
||
if (!searchText) {
|
||
renderDropdown(accounts, false);
|
||
} else {
|
||
inputElement.dispatchEvent(new Event("input"));
|
||
}
|
||
});
|
||
|
||
inputElement.addEventListener("click", () => {
|
||
if (!dropdown.classList.contains("active")) {
|
||
const searchText = inputElement.value.toLowerCase().trim();
|
||
if (!searchText) {
|
||
renderDropdown(accounts, false);
|
||
} else {
|
||
inputElement.dispatchEvent(new Event("input"));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ========================================
|
||
// 初期化
|
||
// ========================================
|
||
async function init() {
|
||
console.log("🔧 init() 开始...");
|
||
const res = await fetch(`${API}/accounts`);
|
||
if (!res.ok) {
|
||
console.error("❌ API 错误:", res.status);
|
||
showAlert("科目一覧の取得に失敗しました", "error");
|
||
return;
|
||
}
|
||
|
||
const data = await res.json();
|
||
console.log("✓ 获取到科目数据:", data);
|
||
|
||
accounts = data.items ?? data;
|
||
console.log("✓ accounts 已赋值,共", accounts.length, "个科目");
|
||
|
||
// 仮払消費税等・仮受消費税等を自動検出
|
||
taxPaidAccounts = accounts.filter((a) =>
|
||
a.account_name.includes("仮払消費税"),
|
||
);
|
||
taxReceivedAccounts = accounts.filter((a) =>
|
||
a.account_name.includes("仮受消費税"),
|
||
);
|
||
|
||
// 科目・取引手段の検索セットアップ
|
||
setupEntryAccountSearch(document.getElementById("entryAccountInput"));
|
||
setupEntryAccountSearch(document.getElementById("entryMethodInput"));
|
||
|
||
setupSearchAccountSelector();
|
||
|
||
document.getElementById("entryDate").value = new Date()
|
||
.toISOString()
|
||
.slice(0, 10);
|
||
|
||
await onEntryDateChanged();
|
||
|
||
// 摘要履歴を読み込み
|
||
loadDescriptionHistory();
|
||
|
||
// 摘要検索ドロップダウンをセットアップ
|
||
setupDescriptionSearch();
|
||
|
||
// 詳細入力セクション初期化
|
||
setupDetailDescriptionSearch();
|
||
detailClearAll();
|
||
// 今日の日付をデフォルト設定
|
||
document.getElementById("detailEntryDate").value = new Date()
|
||
.toISOString()
|
||
.slice(0, 10);
|
||
|
||
console.log("✓ init() 完成");
|
||
}
|
||
|
||
init();
|
||
|
||
// -----------------------------
|
||
// 検索用科目セレクタ
|
||
// -----------------------------
|
||
function setupSearchAccountSelector() {
|
||
// 新的可搜索输入框实现
|
||
const inputElement = document.getElementById("searchAccountInput");
|
||
if (!inputElement) {
|
||
console.log("searchAccountInput 不存在");
|
||
return;
|
||
}
|
||
|
||
const container = inputElement.closest(".account-search-container");
|
||
if (!container) return;
|
||
|
||
const dropdown = container.querySelector(".account-dropdown");
|
||
|
||
function appendSearchOption(acc) {
|
||
const opt = document.createElement("div");
|
||
opt.className = "account-option";
|
||
opt.innerHTML = `<span class="account-option-strong">${acc.account_code}</span><span class="account-option-light">${acc.account_name}</span>`;
|
||
opt.addEventListener("mousedown", (ev) => {
|
||
ev.preventDefault();
|
||
inputElement.value = `${acc.account_code} ${acc.account_name}`;
|
||
inputElement.dataset.accountId = acc.account_id;
|
||
dropdown.classList.remove("active");
|
||
performSearch();
|
||
});
|
||
dropdown.appendChild(opt);
|
||
}
|
||
|
||
function makeSearchSectionLabel(text) {
|
||
const lbl = document.createElement("div");
|
||
lbl.style.cssText =
|
||
"padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
|
||
lbl.textContent = text;
|
||
return lbl;
|
||
}
|
||
|
||
function renderSearchDropdown(list, isFiltered) {
|
||
dropdown.innerHTML = "";
|
||
|
||
// 「すべて」選択肢
|
||
const all = document.createElement("div");
|
||
all.className = "account-option";
|
||
all.innerHTML = `<span class="account-option-strong">-- すべて --</span>`;
|
||
all.addEventListener("mousedown", (ev) => {
|
||
ev.preventDefault();
|
||
inputElement.value = "";
|
||
inputElement.dataset.accountId = "";
|
||
dropdown.classList.remove("active");
|
||
performSearch();
|
||
});
|
||
dropdown.appendChild(all);
|
||
|
||
if (list.length === 0) {
|
||
const none = document.createElement("div");
|
||
none.style.cssText = "padding:8px 12px;color:#999;";
|
||
none.textContent = "該当する科目がありません";
|
||
dropdown.appendChild(none);
|
||
dropdown.classList.add("active");
|
||
return;
|
||
}
|
||
|
||
if (isFiltered) {
|
||
const sorted = list
|
||
.slice()
|
||
.sort((a, b) => a.account_code.localeCompare(b.account_code));
|
||
sorted.slice(0, 50).forEach(appendSearchOption);
|
||
} else {
|
||
const recentIds = JSON.parse(
|
||
localStorage.getItem("recentAccounts") || "[]",
|
||
);
|
||
const recentItems = recentIds
|
||
.map((id) => list.find((a) => a.account_id === id))
|
||
.filter(Boolean);
|
||
const recentSet = new Set(recentIds);
|
||
const otherItems = list
|
||
.filter((a) => !recentSet.has(a.account_id))
|
||
.sort((a, b) => a.account_code.localeCompare(b.account_code));
|
||
|
||
if (recentItems.length > 0) {
|
||
dropdown.appendChild(makeSearchSectionLabel("⭐ 最近使った科目"));
|
||
recentItems.forEach(appendSearchOption);
|
||
const sep = document.createElement("div");
|
||
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
|
||
dropdown.appendChild(sep);
|
||
}
|
||
dropdown.appendChild(makeSearchSectionLabel("📋 すべての科目"));
|
||
otherItems.slice(0, 50).forEach(appendSearchOption);
|
||
}
|
||
|
||
dropdown.classList.add("active");
|
||
}
|
||
|
||
inputElement.addEventListener("input", (e) => {
|
||
const searchText = e.target.value.toLowerCase().trim();
|
||
inputElement.dataset.accountId = "";
|
||
|
||
if (!searchText) {
|
||
renderSearchDropdown(accounts, false);
|
||
return;
|
||
}
|
||
|
||
const filtered = accounts.filter((acc) => {
|
||
const code = acc.account_code.toLowerCase();
|
||
const name = acc.account_name.toLowerCase();
|
||
return (
|
||
code.includes(searchText) ||
|
||
name.includes(searchText) ||
|
||
`${code} ${name}`.includes(searchText)
|
||
);
|
||
});
|
||
|
||
renderSearchDropdown(filtered, true);
|
||
});
|
||
|
||
document.addEventListener("click", (e) => {
|
||
if (!container.contains(e.target)) {
|
||
dropdown.classList.remove("active");
|
||
}
|
||
});
|
||
|
||
inputElement.addEventListener("focus", () => {
|
||
const searchText = inputElement.value.toLowerCase().trim();
|
||
if (!searchText) {
|
||
renderSearchDropdown(accounts, false);
|
||
} else {
|
||
inputElement.dispatchEvent(new Event("input"));
|
||
}
|
||
});
|
||
|
||
inputElement.addEventListener("click", () => {
|
||
if (!dropdown.classList.contains("active")) {
|
||
const searchText = inputElement.value.toLowerCase().trim();
|
||
if (!searchText) {
|
||
renderSearchDropdown(accounts, false);
|
||
} else {
|
||
inputElement.dispatchEvent(new Event("input"));
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// -----------------------------
|
||
// 摘要履歴管理
|
||
// -----------------------------
|
||
function loadDescriptionHistory() {
|
||
// 旧datalist方式の互換用に残す(現在はカスタムドロップダウンで管理)
|
||
}
|
||
|
||
function saveDescriptionToHistory(description) {
|
||
if (!description || description.trim() === "") return;
|
||
|
||
let history = JSON.parse(
|
||
localStorage.getItem("descriptionHistory") || "[]",
|
||
);
|
||
|
||
// 重複を避け、先頭に追加
|
||
history = history.filter((d) => d !== description);
|
||
history.unshift(description);
|
||
|
||
// 最大500件まで保存
|
||
if (history.length > 500) {
|
||
history = history.slice(0, 500);
|
||
}
|
||
|
||
localStorage.setItem("descriptionHistory", JSON.stringify(history));
|
||
}
|
||
|
||
// 摘要検索ドロップダウン
|
||
function setupDescriptionSearch() {
|
||
const input = document.getElementById("description");
|
||
const dropdown = document.getElementById("descriptionDropdown");
|
||
if (!input || !dropdown) return;
|
||
|
||
let debounceTimer = null;
|
||
|
||
function makeSectionLabel(text) {
|
||
const lbl = document.createElement("div");
|
||
lbl.style.cssText =
|
||
"padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
|
||
lbl.textContent = text;
|
||
return lbl;
|
||
}
|
||
|
||
function addOption(text, onSelect) {
|
||
const opt = document.createElement("div");
|
||
opt.className = "account-option";
|
||
opt.style.cssText = "font-size:14px;";
|
||
opt.textContent = text;
|
||
opt.addEventListener("mousedown", (ev) => {
|
||
ev.preventDefault();
|
||
input.value = text;
|
||
dropdown.classList.remove("active");
|
||
if (onSelect) onSelect();
|
||
});
|
||
dropdown.appendChild(opt);
|
||
}
|
||
|
||
function renderDropdown(historyItems, apiItems) {
|
||
dropdown.innerHTML = "";
|
||
|
||
// 充てる候補なし
|
||
if (historyItems.length === 0 && apiItems.length === 0) {
|
||
dropdown.classList.remove("active");
|
||
return;
|
||
}
|
||
|
||
if (historyItems.length > 0) {
|
||
dropdown.appendChild(makeSectionLabel("🗒️ 入力履歴"));
|
||
historyItems.slice(0, 20).forEach((t) => addOption(t));
|
||
}
|
||
|
||
if (apiItems.length > 0) {
|
||
const sep = document.createElement("div");
|
||
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
|
||
if (historyItems.length > 0) dropdown.appendChild(sep);
|
||
dropdown.appendChild(makeSectionLabel("📝 過去の仕訳から"));
|
||
apiItems.slice(0, 20).forEach((t) => addOption(t));
|
||
}
|
||
|
||
dropdown.classList.add("active");
|
||
}
|
||
|
||
async function doSearch(text) {
|
||
const history = JSON.parse(
|
||
localStorage.getItem("descriptionHistory") || "[]",
|
||
);
|
||
const historyFiltered = text
|
||
? history.filter((d) =>
|
||
d.toLowerCase().includes(text.toLowerCase()),
|
||
)
|
||
: history.slice(0, 30);
|
||
|
||
// APIから過去仕訳の摘要を検索
|
||
let apiItems = [];
|
||
try {
|
||
const qs = new URLSearchParams();
|
||
if (text) qs.append("keyword", text);
|
||
qs.append("limit", "50");
|
||
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
const entries = data.items ?? data;
|
||
const historySet = new Set(historyFiltered);
|
||
const seen = new Set();
|
||
apiItems = entries
|
||
.map((e) => e.description)
|
||
.filter(
|
||
(d) => d && !historySet.has(d) && !seen.has(d) && seen.add(d),
|
||
)
|
||
.slice(0, 20);
|
||
}
|
||
} catch (_) {
|
||
// APIエラーは無視
|
||
}
|
||
|
||
renderDropdown(historyFiltered, apiItems);
|
||
}
|
||
|
||
input.addEventListener("input", () => {
|
||
clearTimeout(debounceTimer);
|
||
debounceTimer = setTimeout(() => doSearch(input.value.trim()), 250);
|
||
});
|
||
|
||
input.addEventListener("focus", () => {
|
||
doSearch(input.value.trim());
|
||
});
|
||
|
||
input.addEventListener("click", () => {
|
||
if (!dropdown.classList.contains("active")) {
|
||
doSearch(input.value.trim());
|
||
}
|
||
});
|
||
|
||
document.addEventListener("click", (e) => {
|
||
if (!input.parentElement.contains(e.target)) {
|
||
dropdown.classList.remove("active");
|
||
}
|
||
});
|
||
|
||
// Enterキー・Tabキーでドロップダウンを閉じる
|
||
input.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter" || e.key === "Tab") {
|
||
dropdown.classList.remove("active");
|
||
}
|
||
});
|
||
}
|
||
|
||
// -----------------------------
|
||
// (setupTaxSelectors, addRow, handleTax, updateTotals は新UIでは不要)
|
||
// -----------------------------
|
||
|
||
// ============================================================
|
||
// 仕訳詳細入力(多行対応)
|
||
// ============================================================
|
||
let detailRowSeq = 0;
|
||
|
||
function detailAddLine() {
|
||
const tbody = document.getElementById("detailLinesTbody");
|
||
const rowId = ++detailRowSeq;
|
||
const tr = document.createElement("tr");
|
||
tr.id = `detailRow_${rowId}`;
|
||
tr.innerHTML = `
|
||
<td style="border:1px solid #ccc;padding:4px 6px;">
|
||
<div class="account-search-container" style="position:relative;">
|
||
<input type="text" class="account-search-input detail-account-input"
|
||
data-account-id="" autocomplete="off"
|
||
placeholder="科目を検索" style="width:100%;font-size:13px;" />
|
||
<div class="account-dropdown"></div>
|
||
</div>
|
||
</td>
|
||
<td style="border:1px solid #ccc;padding:4px 6px;">
|
||
<input type="text" class="detail-debit right" inputmode="numeric"
|
||
style="width:100%;text-align:right;padding:5px;box-sizing:border-box;ime-mode:inactive;"
|
||
placeholder="0" oninput="formatNumberInput(this);detailUpdateTotals();" />
|
||
</td>
|
||
<td style="border:1px solid #ccc;padding:4px 6px;">
|
||
<input type="text" class="detail-credit right" inputmode="numeric"
|
||
style="width:100%;text-align:right;padding:5px;box-sizing:border-box;ime-mode:inactive;"
|
||
placeholder="0" oninput="formatNumberInput(this);detailUpdateTotals();" />
|
||
</td>
|
||
<td style="border:1px solid #ccc;padding:4px 6px;">
|
||
<input type="text" class="detail-line-memo"
|
||
style="width:100%;padding:5px;box-sizing:border-box;font-size:12px;"
|
||
placeholder="行摘要(任意)" />
|
||
</td>
|
||
<td style="border:1px solid #ccc;padding:4px 2px;text-align:center;">
|
||
<button onclick="detailRemoveLine(${rowId})"
|
||
style="padding:4px 10px;background:#dc3545;color:white;border:none;border-radius:4px;cursor:pointer;font-size:12px;">削除</button>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
|
||
// 科目検索セットアップ
|
||
const input = tr.querySelector(".detail-account-input");
|
||
setupDetailAccountSearch(input);
|
||
|
||
detailUpdateTotals();
|
||
return tr;
|
||
}
|
||
|
||
function detailRemoveLine(rowId) {
|
||
const tr = document.getElementById(`detailRow_${rowId}`);
|
||
if (tr) tr.remove();
|
||
detailUpdateTotals();
|
||
}
|
||
|
||
function detailGetNumber(input) {
|
||
return Number((input.value || "0").replace(/,/g, "")) || 0;
|
||
}
|
||
|
||
function detailUpdateTotals() {
|
||
const tbody = document.getElementById("detailLinesTbody");
|
||
let debitSum = 0,
|
||
creditSum = 0;
|
||
tbody.querySelectorAll("tr").forEach((tr) => {
|
||
debitSum += detailGetNumber(tr.querySelector(".detail-debit"));
|
||
creditSum += detailGetNumber(tr.querySelector(".detail-credit"));
|
||
});
|
||
|
||
const fmt = (n) => n.toLocaleString();
|
||
document.getElementById("detailDebitTotal").textContent = fmt(debitSum);
|
||
document.getElementById("detailCreditTotal").textContent =
|
||
fmt(creditSum);
|
||
|
||
const el = document.getElementById("detailBalanceStatus");
|
||
if (debitSum === 0 && creditSum === 0) {
|
||
el.textContent = "";
|
||
} else if (debitSum === creditSum) {
|
||
el.textContent = "✅ 貸借一致";
|
||
el.style.color = "#28a745";
|
||
} else {
|
||
const diff = Math.abs(debitSum - creditSum);
|
||
el.textContent = `⚠ 差額 ${diff.toLocaleString()} 円`;
|
||
el.style.color = "#dc3545";
|
||
}
|
||
}
|
||
|
||
function setupDetailAccountSearch(inputElement) {
|
||
const container = inputElement.closest(".account-search-container");
|
||
const dropdown = container.querySelector(".account-dropdown");
|
||
|
||
function makeLbl(text) {
|
||
const lbl = document.createElement("div");
|
||
lbl.style.cssText =
|
||
"padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
|
||
lbl.textContent = text;
|
||
return lbl;
|
||
}
|
||
|
||
function appendOpt(acc) {
|
||
const opt = document.createElement("div");
|
||
opt.className = "account-option";
|
||
opt.innerHTML = `<span class="account-option-strong">${acc.account_code}</span><span class="account-option-light">${acc.account_name}</span>`;
|
||
opt.addEventListener("mousedown", (ev) => {
|
||
ev.preventDefault();
|
||
inputElement.value = `${acc.account_code} ${acc.account_name}`;
|
||
inputElement.dataset.accountId = acc.account_id;
|
||
dropdown.classList.remove("active");
|
||
saveAccountToRecent(acc.account_id);
|
||
});
|
||
dropdown.appendChild(opt);
|
||
}
|
||
|
||
function renderDropdown(list, isFiltered) {
|
||
dropdown.innerHTML = "";
|
||
if (list.length === 0) {
|
||
dropdown.innerHTML =
|
||
'<div style="padding:8px 12px;color:#999;">該当する科目がありません</div>';
|
||
dropdown.classList.add("active");
|
||
return;
|
||
}
|
||
if (isFiltered) {
|
||
list
|
||
.slice()
|
||
.sort((a, b) => a.account_code.localeCompare(b.account_code))
|
||
.slice(0, 50)
|
||
.forEach(appendOpt);
|
||
} else {
|
||
const recentIds = JSON.parse(
|
||
localStorage.getItem("recentAccounts") || "[]",
|
||
);
|
||
const recentItems = recentIds
|
||
.map((id) => list.find((a) => a.account_id === id))
|
||
.filter(Boolean);
|
||
const recentSet = new Set(recentIds);
|
||
const otherItems = list
|
||
.filter((a) => !recentSet.has(a.account_id))
|
||
.sort((a, b) => a.account_code.localeCompare(b.account_code));
|
||
if (recentItems.length > 0) {
|
||
dropdown.appendChild(makeLbl("⭐ 最近使った科目"));
|
||
recentItems.forEach(appendOpt);
|
||
const sep = document.createElement("div");
|
||
sep.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
|
||
dropdown.appendChild(sep);
|
||
}
|
||
dropdown.appendChild(makeLbl("📋 すべての科目"));
|
||
otherItems.slice(0, 50).forEach(appendOpt);
|
||
}
|
||
dropdown.classList.add("active");
|
||
}
|
||
|
||
inputElement.addEventListener("input", (e) => {
|
||
const q = e.target.value.toLowerCase().trim();
|
||
inputElement.dataset.accountId = "";
|
||
const filtered = q
|
||
? accounts.filter(
|
||
(a) =>
|
||
a.account_code.toLowerCase().includes(q) ||
|
||
a.account_name.toLowerCase().includes(q) ||
|
||
`${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes(
|
||
q,
|
||
),
|
||
)
|
||
: accounts;
|
||
renderDropdown(filtered, !!q);
|
||
});
|
||
|
||
inputElement.addEventListener("focus", () => {
|
||
const q = inputElement.value.toLowerCase().trim();
|
||
renderDropdown(
|
||
q
|
||
? accounts.filter(
|
||
(a) =>
|
||
a.account_code.toLowerCase().includes(q) ||
|
||
a.account_name.toLowerCase().includes(q) ||
|
||
`${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes(
|
||
q,
|
||
),
|
||
)
|
||
: accounts,
|
||
!!q,
|
||
);
|
||
});
|
||
|
||
inputElement.addEventListener("click", () => {
|
||
if (!dropdown.classList.contains("active")) {
|
||
const q = inputElement.value.toLowerCase().trim();
|
||
renderDropdown(
|
||
q
|
||
? accounts.filter(
|
||
(a) =>
|
||
a.account_code.toLowerCase().includes(q) ||
|
||
a.account_name.toLowerCase().includes(q) ||
|
||
`${a.account_code.toLowerCase()} ${a.account_name.toLowerCase()}`.includes(
|
||
q,
|
||
),
|
||
)
|
||
: accounts,
|
||
!!q,
|
||
);
|
||
}
|
||
});
|
||
|
||
document.addEventListener("click", (e) => {
|
||
if (!container.contains(e.target))
|
||
dropdown.classList.remove("active");
|
||
});
|
||
}
|
||
|
||
// 詳細入力の摘要オートコンプリート
|
||
function setupDetailDescriptionSearch() {
|
||
const input = document.getElementById("detailDescription");
|
||
const dropdown = document.getElementById("detailDescriptionDropdown");
|
||
if (!input || !dropdown) return;
|
||
|
||
let timer = null;
|
||
|
||
function makeSectionLabel(text) {
|
||
const lbl = document.createElement("div");
|
||
lbl.style.cssText =
|
||
"padding:3px 12px;font-size:11px;color:#888;background:#f5f5f5;font-weight:bold;border-bottom:1px solid #e0e0e0;";
|
||
lbl.textContent = text;
|
||
return lbl;
|
||
}
|
||
|
||
function addOpt(text) {
|
||
const opt = document.createElement("div");
|
||
opt.className = "account-option";
|
||
opt.style.fontSize = "14px";
|
||
opt.textContent = text;
|
||
opt.addEventListener("mousedown", (ev) => {
|
||
ev.preventDefault();
|
||
input.value = text;
|
||
dropdown.classList.remove("active");
|
||
});
|
||
dropdown.appendChild(opt);
|
||
}
|
||
|
||
async function doSearch(text) {
|
||
const history = JSON.parse(
|
||
localStorage.getItem("descriptionHistory") || "[]",
|
||
);
|
||
const histFiltered = text
|
||
? history.filter((d) =>
|
||
d.toLowerCase().includes(text.toLowerCase()),
|
||
)
|
||
: history.slice(0, 30);
|
||
let apiItems = [];
|
||
try {
|
||
const qs = new URLSearchParams();
|
||
if (text) qs.append("keyword", text);
|
||
qs.append("limit", "50");
|
||
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
|
||
if (res.ok) {
|
||
const data = await res.json();
|
||
const entries = data.items ?? data;
|
||
const hSet = new Set(histFiltered);
|
||
const seen = new Set();
|
||
apiItems = entries
|
||
.map((e) => e.description)
|
||
.filter((d) => d && !hSet.has(d) && !seen.has(d) && seen.add(d))
|
||
.slice(0, 20);
|
||
}
|
||
} catch (_) {}
|
||
|
||
dropdown.innerHTML = "";
|
||
if (histFiltered.length === 0 && apiItems.length === 0) {
|
||
dropdown.classList.remove("active");
|
||
return;
|
||
}
|
||
if (histFiltered.length > 0) {
|
||
dropdown.appendChild(makeSectionLabel("🗒️ 入力履歴"));
|
||
histFiltered.slice(0, 20).forEach(addOpt);
|
||
}
|
||
if (apiItems.length > 0) {
|
||
if (histFiltered.length > 0) {
|
||
const s = document.createElement("div");
|
||
s.style.cssText = "border-top:1px solid #e8e8e8;margin:2px 0;";
|
||
dropdown.appendChild(s);
|
||
}
|
||
dropdown.appendChild(makeSectionLabel("📝 過去の仕訳から"));
|
||
apiItems.forEach(addOpt);
|
||
}
|
||
dropdown.classList.add("active");
|
||
}
|
||
|
||
input.addEventListener("input", () => {
|
||
clearTimeout(timer);
|
||
timer = setTimeout(() => doSearch(input.value.trim()), 250);
|
||
});
|
||
input.addEventListener("focus", () => doSearch(input.value.trim()));
|
||
input.addEventListener("click", () => {
|
||
if (!dropdown.classList.contains("active"))
|
||
doSearch(input.value.trim());
|
||
});
|
||
input.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter" || e.key === "Tab")
|
||
dropdown.classList.remove("active");
|
||
});
|
||
document.addEventListener("click", (e) => {
|
||
if (!input.parentElement.contains(e.target))
|
||
dropdown.classList.remove("active");
|
||
});
|
||
}
|
||
|
||
async function submitDetailJournal() {
|
||
const dateInput = document.getElementById("detailEntryDate");
|
||
const entryDate = dateInput.value;
|
||
const description = document
|
||
.getElementById("detailDescription")
|
||
.value.trim();
|
||
|
||
if (dateInput.validity.badInput) {
|
||
showAlert(
|
||
"日付が正しくありません。存在しない日付が入力されています(例:11月31日は無効です)",
|
||
"warning",
|
||
);
|
||
return;
|
||
}
|
||
if (!entryDate) {
|
||
showAlert("仕訳日を入力してください", "warning");
|
||
return;
|
||
}
|
||
if (!description) {
|
||
showAlert("摘要を入力してください", "warning");
|
||
return;
|
||
}
|
||
|
||
const lines = [];
|
||
let debitSum = 0,
|
||
creditSum = 0;
|
||
let valid = true;
|
||
|
||
document.querySelectorAll("#detailLinesTbody tr").forEach((tr) => {
|
||
const accountInput = tr.querySelector(".detail-account-input");
|
||
const accountId = Number(accountInput?.dataset.accountId || 0);
|
||
const debit = detailGetNumber(tr.querySelector(".detail-debit"));
|
||
const credit = detailGetNumber(tr.querySelector(".detail-credit"));
|
||
|
||
if (debit === 0 && credit === 0) return; // 空行スキップ
|
||
|
||
if (!accountId) {
|
||
showAlert("科目が選択されていない行があります", "warning");
|
||
valid = false;
|
||
return;
|
||
}
|
||
if (debit > 0 && credit > 0) {
|
||
showAlert(
|
||
"1行に借方と貸方の両方を入力することはできません",
|
||
"warning",
|
||
);
|
||
valid = false;
|
||
return;
|
||
}
|
||
|
||
const lineDesc =
|
||
tr.querySelector(".detail-line-memo")?.value.trim() || null;
|
||
lines.push({
|
||
account_id: accountId,
|
||
debit,
|
||
credit,
|
||
line_description: lineDesc || undefined,
|
||
});
|
||
debitSum += debit;
|
||
creditSum += credit;
|
||
});
|
||
|
||
if (!valid) return;
|
||
if (lines.length < 2) {
|
||
showAlert("仕訳明細は2行以上必要です", "warning");
|
||
return;
|
||
}
|
||
if (debitSum !== creditSum) {
|
||
showAlert(
|
||
`貸借が一致していません(差額:${Math.abs(debitSum - creditSum).toLocaleString()} 円)`,
|
||
"warning",
|
||
);
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
entry_date: entryDate,
|
||
description,
|
||
lines,
|
||
tax_paid_account_id:
|
||
taxPaidAccounts.length > 0 ? taxPaidAccounts[0].account_id : null,
|
||
tax_received_account_id:
|
||
taxReceivedAccounts.length > 0
|
||
? taxReceivedAccounts[0].account_id
|
||
: null,
|
||
rounding_mode: "floor",
|
||
};
|
||
|
||
try {
|
||
const res = await fetch(`${API}/journal-entries`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.detail || "登録失敗");
|
||
}
|
||
|
||
saveDescriptionToHistory(description);
|
||
showAlert("仕訳を登録しました", "success");
|
||
|
||
const keep = document.getElementById("detailKeepAfterSubmit").checked;
|
||
if (!keep) {
|
||
detailClearAll();
|
||
}
|
||
// keep=true の場合は仕訳日・摘要・明細・金額すべてそのまま保持
|
||
|
||
searchJournals();
|
||
} catch (e) {
|
||
showAlert("登録エラー: " + e.message, "error");
|
||
}
|
||
}
|
||
|
||
function detailClearAll() {
|
||
document.getElementById("detailLinesTbody").innerHTML = "";
|
||
document.getElementById("detailDescription").value = "";
|
||
document.getElementById("detailEntryDate").value = new Date()
|
||
.toISOString()
|
||
.slice(0, 10);
|
||
detailRowSeq = 0;
|
||
detailUpdateTotals();
|
||
// デフォルト2行追加
|
||
detailAddLine();
|
||
detailAddLine();
|
||
}
|
||
|
||
async function submitJournal() {
|
||
try {
|
||
const entryDateInput = document.getElementById("entryDate");
|
||
const entryDate = entryDateInput.value;
|
||
const description = document
|
||
.getElementById("description")
|
||
.value.trim();
|
||
const entryType = document.querySelector(
|
||
'input[name="entryType"]:checked',
|
||
).value;
|
||
|
||
if (entryDateInput.validity.badInput) {
|
||
showAlert(
|
||
"日付が正しくありません。存在しない日付が入力されています(例:11月31日は無効です)",
|
||
"warning",
|
||
);
|
||
return;
|
||
}
|
||
if (!entryDate) {
|
||
showAlert("仕訳日を入力してください", "warning");
|
||
return;
|
||
}
|
||
|
||
const accountId = Number(
|
||
document.getElementById("entryAccountInput").dataset.accountId || 0,
|
||
);
|
||
const methodId = Number(
|
||
document.getElementById("entryMethodInput").dataset.accountId || 0,
|
||
);
|
||
const amount = Number(
|
||
document.getElementById("entryAmount").value.replace(/,/g, "") || 0,
|
||
);
|
||
const taxRateVal = document.getElementById("entryTaxRate").value;
|
||
const taxAmount = Number(
|
||
document.getElementById("entryTaxAmount").value.replace(/,/g, "") ||
|
||
0,
|
||
);
|
||
|
||
if (!accountId) {
|
||
showAlert("科目を選択してください", "warning");
|
||
return;
|
||
}
|
||
if (!methodId) {
|
||
showAlert("取引手段を選択してください", "warning");
|
||
return;
|
||
}
|
||
if (amount <= 0) {
|
||
showAlert("金額を入力してください", "warning");
|
||
return;
|
||
}
|
||
|
||
const netAmount = amount - taxAmount; // 税抜金額
|
||
const lines = [];
|
||
|
||
// 消費税率を数値に変換(バックエンド送信用)
|
||
const taxRateNum =
|
||
taxRateVal === "8r"
|
||
? 8
|
||
: taxRateVal === "0"
|
||
? null
|
||
: Number(taxRateVal);
|
||
|
||
if (entryType === "expense") {
|
||
// ===== 支出 =====
|
||
// 借方: 科目(税抜金額)
|
||
lines.push({
|
||
account_id: accountId,
|
||
debit: netAmount,
|
||
credit: 0,
|
||
...(taxRateNum !== null ? { tax_rate: taxRateNum } : {}),
|
||
});
|
||
|
||
// 借方: 仮払消費税等(税額)※税額がある場合のみ
|
||
if (taxAmount > 0) {
|
||
const taxPaidAcc =
|
||
taxPaidAccounts.length > 0 ? taxPaidAccounts[0] : null;
|
||
if (!taxPaidAcc) {
|
||
showAlert("仮払消費税等の勘定科目が見つかりません", "error");
|
||
return;
|
||
}
|
||
lines.push({
|
||
account_id: taxPaidAcc.account_id,
|
||
debit: taxAmount,
|
||
credit: 0,
|
||
});
|
||
}
|
||
|
||
// 貸方: 取引手段(税込金額)
|
||
lines.push({
|
||
account_id: methodId,
|
||
debit: 0,
|
||
credit: amount,
|
||
});
|
||
} else {
|
||
// ===== 収入 =====
|
||
// 借方: 取引手段(税込金額)
|
||
lines.push({
|
||
account_id: methodId,
|
||
debit: amount,
|
||
credit: 0,
|
||
});
|
||
|
||
// 貸方: 科目(税抜金額)
|
||
lines.push({
|
||
account_id: accountId,
|
||
debit: 0,
|
||
credit: netAmount,
|
||
...(taxRateNum !== null ? { tax_rate: taxRateNum } : {}),
|
||
});
|
||
|
||
// 貸方: 仮受消費税等(税額)※税額がある場合のみ
|
||
if (taxAmount > 0) {
|
||
const taxReceivedAcc =
|
||
taxReceivedAccounts.length > 0 ? taxReceivedAccounts[0] : null;
|
||
if (!taxReceivedAcc) {
|
||
showAlert("仮受消費税等の勘定科目が見つかりません", "error");
|
||
return;
|
||
}
|
||
lines.push({
|
||
account_id: taxReceivedAcc.account_id,
|
||
debit: 0,
|
||
credit: taxAmount,
|
||
});
|
||
}
|
||
}
|
||
|
||
const payload = {
|
||
entry_date: entryDate,
|
||
description,
|
||
lines,
|
||
tax_paid_account_id:
|
||
taxPaidAccounts.length > 0 ? taxPaidAccounts[0].account_id : null,
|
||
tax_received_account_id:
|
||
taxReceivedAccounts.length > 0
|
||
? taxReceivedAccounts[0].account_id
|
||
: null,
|
||
rounding_mode:
|
||
document.getElementById("entryRoundingMode")?.value || "floor",
|
||
};
|
||
|
||
console.log("送信するデータ:", JSON.stringify(payload, null, 2));
|
||
|
||
const res = await fetch(`${API}/journal-entries`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const errorData = await res.json().catch(() => ({}));
|
||
const errorMsg =
|
||
errorData.detail || `登録に失敗しました (HTTP ${res.status})`;
|
||
console.error("Error:", errorData);
|
||
showAlert(errorMsg, "error");
|
||
return;
|
||
}
|
||
|
||
const result = await res.json();
|
||
|
||
// 摘要を履歴に保存
|
||
saveDescriptionToHistory(description);
|
||
|
||
showAlert(`登録完了(ID=${result.journal_entry_id})`, "success");
|
||
|
||
// 「登録後に明細を保持する」チェックボックスを確認
|
||
const keepDetails = document.getElementById(
|
||
"keepDetailsAfterSubmit",
|
||
).checked;
|
||
|
||
if (!keepDetails) {
|
||
// フォームをクリア
|
||
document.getElementById("description").value = "";
|
||
document.getElementById("entryAccountInput").value = "";
|
||
document.getElementById("entryAccountInput").dataset.accountId = "";
|
||
document.getElementById("entryMethodInput").value = "";
|
||
document.getElementById("entryMethodInput").dataset.accountId = "";
|
||
document.getElementById("entryAmount").value = "";
|
||
document.getElementById("entryTaxRate").value = "10";
|
||
document.getElementById("entryTaxAmount").value = "";
|
||
}
|
||
|
||
// 仕訳検索を自動実行して最新の登録状況を表示
|
||
searchJournals();
|
||
} catch (error) {
|
||
console.error("送信エラー:", error);
|
||
showAlert(
|
||
`エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。`,
|
||
"error",
|
||
);
|
||
}
|
||
}
|
||
|
||
const API_BASE = "";
|
||
|
||
async function callApi(path) {
|
||
try {
|
||
const res = await fetch(`${API_BASE}${path}`, { method: "POST" });
|
||
const text = await res.text();
|
||
let data = {};
|
||
try {
|
||
data = text ? JSON.parse(text) : {};
|
||
} catch {
|
||
data = { detail: text };
|
||
}
|
||
|
||
if (!res.ok) {
|
||
throw new Error(data.detail || `HTTP ${res.status}`);
|
||
}
|
||
return data;
|
||
} catch (e) {
|
||
throw new Error(e.message || "请求失败");
|
||
}
|
||
}
|
||
|
||
async function lockMonth() {
|
||
const year = document.getElementById("lockYear").value;
|
||
const month = document.getElementById("lockMonth").value;
|
||
|
||
const el = document.getElementById("lockResult");
|
||
el.innerText = "处理中...";
|
||
|
||
try {
|
||
await callApi(`/month-locks/lock?fiscal_year=${year}&month=${month}`);
|
||
el.innerText = `✅ ${year}年${month}月 已锁定`;
|
||
await refreshMonthLocks(); // 可选:自动刷新列表
|
||
} catch (e) {
|
||
el.innerText = `❌ 错误:${e.message}`;
|
||
}
|
||
}
|
||
|
||
async function unlockMonth() {
|
||
const year = document.getElementById("lockYear").value;
|
||
const month = document.getElementById("lockMonth").value;
|
||
|
||
const el = document.getElementById("lockResult");
|
||
el.innerText = "处理中...";
|
||
|
||
try {
|
||
await callApi(
|
||
`/month-locks/unlock?fiscal_year=${year}&month=${month}`,
|
||
);
|
||
el.innerText = `✅ ${year}年${month}月 已解锁`;
|
||
await refreshMonthLocks(); // 可选:自动刷新列表
|
||
} catch (e) {
|
||
el.innerText = `❌ 错误:${e.message}`;
|
||
}
|
||
}
|
||
|
||
async function refreshMonthLocks() {
|
||
const el = document.getElementById("lockList");
|
||
el.textContent = "读取中...";
|
||
|
||
try {
|
||
const res = await fetch(`${API_BASE}/month-locks`);
|
||
const data = await res.json();
|
||
|
||
// 只显示 locked 的,避免太长
|
||
const locked = (data || []).filter((x) => x.is_locked);
|
||
|
||
if (locked.length === 0) {
|
||
el.textContent = "(当前没有锁定月份)";
|
||
return;
|
||
}
|
||
|
||
el.textContent = locked
|
||
.map(
|
||
(x) =>
|
||
`${x.fiscal_year}-${String(x.month).padStart(
|
||
2,
|
||
"0",
|
||
)} locked_by=${x.locked_by ?? ""}`,
|
||
)
|
||
.join("\n");
|
||
} catch (e) {
|
||
el.textContent = `读取失败:${e.message || e}`;
|
||
}
|
||
}
|
||
|
||
async function checkMonthLockByDate(dateStr) {
|
||
if (!dateStr) return false;
|
||
|
||
const d = new Date(dateStr);
|
||
const year = d.getFullYear();
|
||
const month = d.getMonth() + 1;
|
||
|
||
const res = await fetch(`${API_BASE}/month-locks`);
|
||
const data = await res.json();
|
||
|
||
return (data || []).some(
|
||
(x) => x.fiscal_year === year && x.month === month && x.is_locked,
|
||
);
|
||
}
|
||
|
||
async function onEntryDateChanged() {
|
||
const dateStr = document.getElementById("entryDate").value;
|
||
const locked = await checkMonthLockByDate(dateStr);
|
||
|
||
const banner = document.getElementById("monthLockBanner");
|
||
|
||
if (locked) {
|
||
banner.style.display = "block";
|
||
setJournalReadonly(true);
|
||
} else {
|
||
banner.style.display = "none";
|
||
setJournalReadonly(false);
|
||
}
|
||
}
|
||
|
||
function setJournalReadonly(readonly) {
|
||
// 保存按钮
|
||
const saveBtn = document.getElementById("saveButton");
|
||
if (saveBtn) saveBtn.disabled = readonly;
|
||
|
||
// 行追加按钮
|
||
const addRowBtn = document.getElementById("addRowButton");
|
||
if (addRowBtn) addRowBtn.disabled = readonly;
|
||
|
||
// 所有“删除行”按钮
|
||
document.querySelectorAll(".delete-row-btn").forEach((btn) => {
|
||
btn.disabled = readonly;
|
||
});
|
||
|
||
// 所有仕訳输入项(金额 / 科目等)
|
||
document.querySelectorAll(".journal-input").forEach((input) => {
|
||
input.disabled = readonly;
|
||
});
|
||
}
|
||
|
||
function buildAccountOptions() {
|
||
let html = "";
|
||
|
||
// 最近使った科目を表示
|
||
const recentAccountIds = JSON.parse(
|
||
localStorage.getItem("recentAccounts") || "[]",
|
||
);
|
||
if (recentAccountIds.length > 0) {
|
||
html += `<optgroup label="⭐ 最近使った科目">`;
|
||
recentAccountIds.forEach((accountId) => {
|
||
const account = accounts.find((a) => a.account_id === accountId);
|
||
if (account) {
|
||
html += `<option value="${account.account_id}">
|
||
${account.account_code} ${account.account_name}
|
||
</option>`;
|
||
}
|
||
});
|
||
html += `</optgroup>`;
|
||
}
|
||
|
||
// 已分组的科目
|
||
ACCOUNT_GROUPS.forEach((group) => {
|
||
html += `<optgroup label="${group.label}">`;
|
||
accounts
|
||
.filter((a) => group.codes.includes(a.account_code))
|
||
.forEach((a) => {
|
||
html += `<option value="${a.account_id}">
|
||
${a.account_code} ${a.account_name}
|
||
</option>`;
|
||
});
|
||
html += `</optgroup>`;
|
||
});
|
||
|
||
// 未分组的其他科目
|
||
const groupedCodes = ACCOUNT_GROUPS.flatMap((g) => g.codes);
|
||
const others = accounts.filter(
|
||
(a) => !groupedCodes.includes(a.account_code),
|
||
);
|
||
|
||
if (others.length > 0) {
|
||
html += `<optgroup label="📚 その他">`;
|
||
others.forEach((a) => {
|
||
html += `<option value="${a.account_id}">
|
||
${a.account_code} ${a.account_name}
|
||
</option>`;
|
||
});
|
||
html += `</optgroup>`;
|
||
}
|
||
|
||
return html;
|
||
}
|
||
|
||
// 科目選択履歴を保存
|
||
function saveAccountToRecent(accountId) {
|
||
if (!accountId) return;
|
||
|
||
let recentAccounts = JSON.parse(
|
||
localStorage.getItem("recentAccounts") || "[]",
|
||
);
|
||
|
||
// 重複を除去して先頭に追加
|
||
recentAccounts = recentAccounts.filter((id) => id !== accountId);
|
||
recentAccounts.unshift(accountId);
|
||
|
||
// 最大20件まで保持
|
||
if (recentAccounts.length > 20) {
|
||
recentAccounts = recentAccounts.slice(0, 20);
|
||
}
|
||
|
||
localStorage.setItem("recentAccounts", JSON.stringify(recentAccounts));
|
||
}
|
||
|
||
// ========================================
|
||
// 仕訳検索機能(journal-list.htmlから統合)
|
||
// ========================================
|
||
// 検索結果を印刷(検索を実行してから印刷)
|
||
async function printSearchResults() {
|
||
try {
|
||
// まず検索を実行
|
||
const from = document.getElementById("searchFromDate").value;
|
||
const to = document.getElementById("searchToDate").value;
|
||
const key = document.getElementById("searchKeyword").value;
|
||
const accountInput = document.getElementById("searchAccountInput");
|
||
const accountId = accountInput ? accountInput.dataset.accountId : "";
|
||
const accountSide =
|
||
document.getElementById("searchAccountSide").value;
|
||
const includeHistory = document.getElementById(
|
||
"searchIncludeHistory",
|
||
).checked;
|
||
|
||
const qs = new URLSearchParams();
|
||
if (from) qs.append("from_date", from);
|
||
if (to) qs.append("to_date", to);
|
||
if (key) qs.append("keyword", key);
|
||
if (accountId) qs.append("account_id", accountId);
|
||
if (accountSide) qs.append("account_side", accountSide);
|
||
if (includeHistory) qs.append("include_history", "true");
|
||
|
||
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
|
||
if (!res.ok) {
|
||
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
||
}
|
||
let data = await res.json();
|
||
if (!Array.isArray(data)) {
|
||
console.warn("⚠️ API返り値は配列ではありません:", data);
|
||
data = [];
|
||
}
|
||
|
||
// 印刷用に昇順でソート
|
||
const printData = [...data].sort((a, b) => {
|
||
if (a.entry_date !== b.entry_date) {
|
||
return a.entry_date.localeCompare(b.entry_date);
|
||
}
|
||
return a.journal_entry_id - b.journal_entry_id;
|
||
});
|
||
|
||
const printArea = document.getElementById("printArea");
|
||
printArea.innerHTML = "";
|
||
|
||
// 月ごとにデータをグループ化
|
||
const monthGroups = {};
|
||
printData.forEach((e) => {
|
||
const entryMonth = e.entry_date.substring(0, 7);
|
||
if (!monthGroups[entryMonth]) {
|
||
monthGroups[entryMonth] = [];
|
||
}
|
||
monthGroups[entryMonth].push(e);
|
||
});
|
||
|
||
// 検索条件を生成
|
||
const accountName = accountInput ? accountInput.value : "";
|
||
const sideSelect = document.getElementById("searchAccountSide");
|
||
const sideName =
|
||
sideSelect.options[sideSelect.selectedIndex]?.text || "";
|
||
let conditions = [];
|
||
if (from || to) {
|
||
conditions.push(`期間: ${from || ""} ~ ${to || ""}`);
|
||
}
|
||
if (key) {
|
||
conditions.push(`摘要: ${key}`);
|
||
}
|
||
if (accountName && accountName !== "-- すべて --") {
|
||
conditions.push(`科目: ${accountName}`);
|
||
}
|
||
if (sideName && sideName !== "-- すべて --") {
|
||
conditions.push(`方向: ${sideName}`);
|
||
}
|
||
if (includeHistory) {
|
||
conditions.push(`修正履歴を含む`);
|
||
}
|
||
const conditionsText =
|
||
conditions.length > 0 ? conditions.join(" | ") : "検索条件: すべて";
|
||
|
||
// 印刷日時
|
||
const printedAt = new Date().toLocaleString("ja-JP", {
|
||
year: "numeric",
|
||
month: "2-digit",
|
||
day: "2-digit",
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
second: "2-digit",
|
||
});
|
||
|
||
// 月ごとにテーブルを生成(印刷用)
|
||
Object.keys(monthGroups)
|
||
.sort()
|
||
.forEach((month, index) => {
|
||
const monthData = monthGroups[month];
|
||
const monthDisplay = new Date(month + "-01").toLocaleDateString(
|
||
"ja-JP",
|
||
{
|
||
year: "numeric",
|
||
month: "long",
|
||
},
|
||
);
|
||
|
||
const section = document.createElement("div");
|
||
if (index > 0) {
|
||
section.className = "page-break";
|
||
}
|
||
|
||
section.innerHTML = `
|
||
<table style="margin-bottom: 20px;">
|
||
<thead>
|
||
<tr>
|
||
<th colspan="5" style="text-align: center; font-size: 1.2em; padding: 8px; border: none; background: transparent;">仕訳検索結果</th>
|
||
</tr>
|
||
<tr>
|
||
<th colspan="5" style="text-align: right; font-weight: normal; font-size: 0.85em; padding: 2px 4px; border: none; background: transparent;">印刷日時: ${printedAt}</th>
|
||
</tr>
|
||
<tr>
|
||
<th colspan="3" style="text-align: left; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${conditionsText}</th>
|
||
<th colspan="2" style="text-align: right; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${monthDisplay}</th>
|
||
</tr>
|
||
<tr>
|
||
<th style="white-space: nowrap;">日付</th>
|
||
<th>摘要</th>
|
||
<th style="white-space: nowrap;">借方合計</th>
|
||
<th style="white-space: nowrap;">貸方合計</th>
|
||
<th>税率</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
<div class="page-number">ページ ${index + 1}</div>
|
||
`;
|
||
|
||
const tbody = section.querySelector("tbody");
|
||
let monthDebitTotal = 0;
|
||
let monthCreditTotal = 0;
|
||
|
||
monthData.forEach((e) => {
|
||
monthDebitTotal += e.debit_total;
|
||
monthCreditTotal += e.credit_total;
|
||
|
||
// 修正版本の表示(revised_count を使用)
|
||
// includeHistory に関係なく、修正版本は常に表示
|
||
const versionInfo =
|
||
e.revision_count && e.revision_count > 1
|
||
? ` (v${e.revision_count})`
|
||
: "";
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td style="white-space: nowrap;">${e.entry_date}</td>
|
||
<td style="text-align:left">${e.description}${versionInfo}</td>
|
||
<td style="text-align:right; white-space: nowrap;">${e.debit_total.toLocaleString()}</td>
|
||
<td style="text-align:right; white-space: nowrap;">${e.credit_total.toLocaleString()}</td>
|
||
<td style="text-align:center">${
|
||
e.tax_rates ? e.tax_rates + "%" : ""
|
||
}</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
// 月合計行を追加
|
||
const totalRow = document.createElement("tr");
|
||
totalRow.style.fontWeight = "bold";
|
||
totalRow.style.backgroundColor = "#f0f0f0";
|
||
totalRow.innerHTML = `
|
||
<td colspan="2" style="text-align:right; white-space: nowrap;">合計:</td>
|
||
<td style="text-align:right; white-space: nowrap;">${monthDebitTotal.toLocaleString()}</td>
|
||
<td style="text-align:right; white-space: nowrap;">${monthCreditTotal.toLocaleString()}</td>
|
||
<td></td>
|
||
`;
|
||
tbody.appendChild(totalRow);
|
||
|
||
printArea.appendChild(section);
|
||
});
|
||
// 少し待ってから印刷(データの表示を待つ)
|
||
setTimeout(() => {
|
||
window.print();
|
||
}, 100);
|
||
} catch (err) {
|
||
console.error("❌ 印刷用検索エラー:", err);
|
||
const printArea = document.getElementById("printArea");
|
||
if (printArea) {
|
||
printArea.innerHTML = `
|
||
<div style="padding: 20px; color: red; border: 2px solid red;">
|
||
<h3>❌ 検索に失敗しました</h3>
|
||
<p><strong>エラー内容:</strong> ${err.message}</p>
|
||
<p style="font-size: 0.9em; color: #666;">
|
||
F12 で開発者ツールを確認して、コンソールログを確認してください。
|
||
</p>
|
||
</div>
|
||
`;
|
||
}
|
||
showAlert(`検索に失敗しました: ${err.message}`, "error");
|
||
}
|
||
}
|
||
|
||
// 搜索函数(在选择搜索科目时被触发)
|
||
function performSearch() {
|
||
searchJournals();
|
||
}
|
||
|
||
// 检索结果を表に表示
|
||
async function searchJournals() {
|
||
try {
|
||
const from = document.getElementById("searchFromDate").value;
|
||
const to = document.getElementById("searchToDate").value;
|
||
const key = document.getElementById("searchKeyword").value;
|
||
const accountInput = document.getElementById("searchAccountInput");
|
||
const accountId = accountInput ? accountInput.dataset.accountId : "";
|
||
const accountSide =
|
||
document.getElementById("searchAccountSide").value;
|
||
const includeHistory = document.getElementById(
|
||
"searchIncludeHistory",
|
||
).checked;
|
||
|
||
// 検索条件を保存
|
||
localStorage.setItem(
|
||
"journalSearchConditions",
|
||
JSON.stringify({
|
||
fromDate: from,
|
||
toDate: to,
|
||
keyword: key,
|
||
accountId: accountId,
|
||
accountSide: accountSide,
|
||
includeHistory: includeHistory,
|
||
}),
|
||
);
|
||
|
||
const qs = new URLSearchParams();
|
||
if (from) qs.append("from_date", from);
|
||
if (to) qs.append("to_date", to);
|
||
if (key) qs.append("keyword", key);
|
||
if (accountId) qs.append("account_id", accountId);
|
||
if (accountSide) qs.append("account_side", accountSide);
|
||
if (includeHistory) qs.append("include_history", "true");
|
||
|
||
console.log(
|
||
"🔍 検索 URL:",
|
||
`${API}/journal-entries?${qs.toString()}`,
|
||
);
|
||
|
||
const res = await fetch(`${API}/journal-entries?${qs.toString()}`);
|
||
|
||
// ❌ API レスポンス確認
|
||
if (!res.ok) {
|
||
throw new Error(`API error: ${res.status} ${res.statusText}`);
|
||
}
|
||
|
||
let data = await res.json();
|
||
|
||
// 返されたデータが配列か確認
|
||
if (!Array.isArray(data)) {
|
||
console.warn("⚠️ API返り値は配列ではありません:", data);
|
||
data = [];
|
||
}
|
||
|
||
console.log("✓ 検索結果:", data.length, "件");
|
||
|
||
const printArea = document.getElementById("printArea");
|
||
printArea.innerHTML = "";
|
||
|
||
// 月ごとにデータをグループ化
|
||
const monthGroups = {};
|
||
data.forEach((e) => {
|
||
const entryMonth = e.entry_date.substring(0, 7); // YYYY-MM
|
||
if (!monthGroups[entryMonth]) {
|
||
monthGroups[entryMonth] = [];
|
||
}
|
||
monthGroups[entryMonth].push(e);
|
||
});
|
||
|
||
// 検索条件を生成
|
||
const conditionsText = (() => {
|
||
let conditions = [];
|
||
if (from || to) {
|
||
conditions.push(`期間: ${from || ""} ~ ${to || ""}`);
|
||
}
|
||
if (key) {
|
||
conditions.push(`摘要: ${key}`);
|
||
}
|
||
const accountInput = document.getElementById("searchAccountInput");
|
||
const accountName = accountInput ? accountInput.value : "";
|
||
if (accountName && accountName.trim() !== "") {
|
||
conditions.push(`科目: ${accountName}`);
|
||
}
|
||
const sideSelect = document.getElementById("searchAccountSide");
|
||
const sideName = sideSelect
|
||
? sideSelect.options[sideSelect.selectedIndex]?.text
|
||
: "";
|
||
if (sideName && sideName !== "-- すべて --") {
|
||
conditions.push(`方向: ${sideName}`);
|
||
}
|
||
if (includeHistory) {
|
||
conditions.push(`修正履歴を含む`);
|
||
}
|
||
return conditions.length > 0
|
||
? conditions.join(" | ")
|
||
: "検索条件: すべて";
|
||
})();
|
||
|
||
// 月ごとにテーブルを生成(画面表示は降順)
|
||
Object.keys(monthGroups)
|
||
.sort((a, b) => b.localeCompare(a)) // 月を降順で表示
|
||
.forEach((month, index) => {
|
||
const monthData = monthGroups[month];
|
||
const monthDisplay = new Date(month + "-01").toLocaleDateString(
|
||
"ja-JP",
|
||
{
|
||
year: "numeric",
|
||
month: "long",
|
||
},
|
||
);
|
||
|
||
const section = document.createElement("div");
|
||
if (index > 0) {
|
||
section.className = "page-break";
|
||
}
|
||
|
||
section.innerHTML = `
|
||
<table style="margin-bottom: 20px;">
|
||
<thead>
|
||
<tr>
|
||
<th colspan="6" style="text-align: center; font-size: 1.2em; padding: 8px; border: none; background: transparent;">仕訳検索結果</th>
|
||
</tr>
|
||
<tr>
|
||
<th colspan="3" style="text-align: left; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${conditionsText}</th>
|
||
<th colspan="3" style="text-align: right; font-weight: normal; font-size: 0.9em; padding: 4px; border: none; background: transparent;">${monthDisplay}</th>
|
||
</tr>
|
||
<tr>
|
||
<th style="white-space: nowrap;">日付</th>
|
||
<th>摘要</th>
|
||
<th style="white-space: nowrap;">借方合計</th>
|
||
<th style="white-space: nowrap;">貸方合計</th>
|
||
<th>税率</th>
|
||
<th style="white-space: nowrap;">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
<div class="page-number">ページ ${index + 1}</div>
|
||
`;
|
||
|
||
const tbody = section.querySelector("tbody");
|
||
let monthDebitTotal = 0;
|
||
let monthCreditTotal = 0;
|
||
|
||
// 月内のデータも降順で表示(画面表示用)
|
||
monthData.forEach((e) => {
|
||
monthDebitTotal += e.debit_total;
|
||
monthCreditTotal += e.credit_total;
|
||
|
||
// 修正版本の表示(revision_count を使用)
|
||
// includeHistory に関係なく、修正版本は常に表示
|
||
const versionInfo =
|
||
e.revision_count && e.revision_count > 1
|
||
? ` (v${e.revision_count})`
|
||
: "";
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td style="white-space: nowrap;">${e.entry_date}</td>
|
||
<td style="text-align:left">${e.description}${versionInfo}</td>
|
||
<td style="text-align:right; white-space: nowrap;">${e.debit_total.toLocaleString()}</td>
|
||
<td style="text-align:right; white-space: nowrap;">${e.credit_total.toLocaleString()}</td>
|
||
<td style="text-align:center">${
|
||
e.tax_rates ? e.tax_rates + "%" : ""
|
||
}</td>
|
||
<td style="white-space: nowrap;">
|
||
<button onclick="viewJournal(${
|
||
e.journal_entry_id
|
||
})">表示</button>
|
||
<button onclick="copyJournal(${
|
||
e.journal_entry_id
|
||
})" style="color: green;">複製</button>
|
||
<button onclick="reverseAndEdit(${
|
||
e.journal_entry_id
|
||
})">修正</button>
|
||
<button onclick="deleteJournalEntry(${
|
||
e.journal_entry_id
|
||
})" style="color: red;">削除</button>
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
|
||
// 月合計行を追加
|
||
const totalRow = document.createElement("tr");
|
||
totalRow.style.fontWeight = "bold";
|
||
totalRow.style.backgroundColor = "#f0f0f0";
|
||
totalRow.innerHTML = `
|
||
<td colspan="2" style="text-align:right; white-space: nowrap;">合計:</td>
|
||
<td style="text-align:right; white-space: nowrap;">${monthDebitTotal.toLocaleString()}</td>
|
||
<td style="text-align:right; white-space: nowrap;">${monthCreditTotal.toLocaleString()}</td>
|
||
<td colspan="2"></td>
|
||
`;
|
||
tbody.appendChild(totalRow);
|
||
|
||
printArea.appendChild(section);
|
||
});
|
||
} catch (err) {
|
||
console.error("❌ 検索エラー:", err);
|
||
const printArea = document.getElementById("printArea");
|
||
if (printArea) {
|
||
printArea.innerHTML = `
|
||
<div style="padding: 20px; color: red; border: 2px solid red;">
|
||
<h3>❌ 検索に失敗しました</h3>
|
||
<p><strong>エラー内容:</strong> ${err.message}</p>
|
||
<p style="font-size: 0.9em; color: #666;">
|
||
F12 で開発者ツールを確認して、コンソールログを確認してください。
|
||
</p>
|
||
</div>
|
||
`;
|
||
}
|
||
showAlert(`検索に失敗しました: ${err.message}`, "error");
|
||
}
|
||
}
|
||
|
||
function viewJournal(id) {
|
||
window.open(`journal-view.html?id=${id}`, "_blank");
|
||
}
|
||
|
||
// --------------------------------------------------
|
||
// 仕訳複製:既存の仕訳を入力フォームにコピー
|
||
// --------------------------------------------------
|
||
async function copyJournal(id) {
|
||
try {
|
||
const res = await fetch(`${API}/journal-entries/${id}`);
|
||
if (!res.ok) {
|
||
showAlert("仕訳の取得に失敗しました", "error");
|
||
return;
|
||
}
|
||
|
||
const data = await res.json();
|
||
|
||
// 日付をコピー
|
||
document.getElementById("entryDate").value = data.entry_date;
|
||
|
||
// 摘要をコピー
|
||
document.getElementById("description").value = data.description || "";
|
||
|
||
// 新UIでは簡易的にフォームに反映(最初の借方行の科目 + 最初の貸方行を取引手段に設定)
|
||
if (data.lines && data.lines.length > 0) {
|
||
// 借方と貸方を分類
|
||
const debitLines = data.lines.filter((l) => Number(l.debit) > 0);
|
||
const creditLines = data.lines.filter((l) => Number(l.credit) > 0);
|
||
|
||
// 仮払/仮受消費税行を除外して主要な科目を特定
|
||
const mainDebit = debitLines.find((l) => {
|
||
const acc = accounts.find((a) => a.account_id === l.account_id);
|
||
return (
|
||
acc &&
|
||
!acc.account_name.includes("仮払消費税") &&
|
||
!acc.account_name.includes("仮受消費税")
|
||
);
|
||
});
|
||
const mainCredit = creditLines.find((l) => {
|
||
const acc = accounts.find((a) => a.account_id === l.account_id);
|
||
return (
|
||
acc &&
|
||
!acc.account_name.includes("仮払消費税") &&
|
||
!acc.account_name.includes("仮受消費税")
|
||
);
|
||
});
|
||
|
||
// 税額行を検出
|
||
const taxDebitLine = debitLines.find((l) => {
|
||
const acc = accounts.find((a) => a.account_id === l.account_id);
|
||
return acc && acc.account_name.includes("仮払消費税");
|
||
});
|
||
const taxCreditLine = creditLines.find((l) => {
|
||
const acc = accounts.find((a) => a.account_id === l.account_id);
|
||
return acc && acc.account_name.includes("仮受消費税");
|
||
});
|
||
|
||
if (mainDebit && mainCredit) {
|
||
// 支出パターン: 借方=科目, 貸方=取引手段
|
||
if (taxDebitLine) {
|
||
// 支出(仮払消費税あり)
|
||
document.querySelector(
|
||
'input[name="entryType"][value="expense"]',
|
||
).checked = true;
|
||
const kamokuAcc = accounts.find(
|
||
(a) => a.account_id === mainDebit.account_id,
|
||
);
|
||
const methodAcc = accounts.find(
|
||
(a) => a.account_id === mainCredit.account_id,
|
||
);
|
||
if (kamokuAcc) {
|
||
document.getElementById("entryAccountInput").value =
|
||
`${kamokuAcc.account_code} ${kamokuAcc.account_name}`;
|
||
document.getElementById(
|
||
"entryAccountInput",
|
||
).dataset.accountId = kamokuAcc.account_id;
|
||
}
|
||
if (methodAcc) {
|
||
document.getElementById("entryMethodInput").value =
|
||
`${methodAcc.account_code} ${methodAcc.account_name}`;
|
||
document.getElementById(
|
||
"entryMethodInput",
|
||
).dataset.accountId = methodAcc.account_id;
|
||
}
|
||
const totalAmount = Number(mainCredit.credit);
|
||
document.getElementById("entryAmount").value =
|
||
totalAmount.toLocaleString();
|
||
document.getElementById("entryTaxAmount").value = Number(
|
||
taxDebitLine.debit,
|
||
).toLocaleString();
|
||
} else if (taxCreditLine) {
|
||
// 収入(仮受消費税あり)
|
||
document.querySelector(
|
||
'input[name="entryType"][value="income"]',
|
||
).checked = true;
|
||
const kamokuAcc = accounts.find(
|
||
(a) => a.account_id === mainCredit.account_id,
|
||
);
|
||
const methodAcc = accounts.find(
|
||
(a) => a.account_id === mainDebit.account_id,
|
||
);
|
||
if (kamokuAcc) {
|
||
document.getElementById("entryAccountInput").value =
|
||
`${kamokuAcc.account_code} ${kamokuAcc.account_name}`;
|
||
document.getElementById(
|
||
"entryAccountInput",
|
||
).dataset.accountId = kamokuAcc.account_id;
|
||
}
|
||
if (methodAcc) {
|
||
document.getElementById("entryMethodInput").value =
|
||
`${methodAcc.account_code} ${methodAcc.account_name}`;
|
||
document.getElementById(
|
||
"entryMethodInput",
|
||
).dataset.accountId = methodAcc.account_id;
|
||
}
|
||
const totalAmount = Number(mainDebit.debit);
|
||
document.getElementById("entryAmount").value =
|
||
totalAmount.toLocaleString();
|
||
document.getElementById("entryTaxAmount").value = Number(
|
||
taxCreditLine.credit,
|
||
).toLocaleString();
|
||
} else {
|
||
// 消費税なし - 借方科目が費用/資産 → 支出、それ以外は収入
|
||
const kamokuAccD = accounts.find(
|
||
(a) => a.account_id === mainDebit.account_id,
|
||
);
|
||
const isExpense =
|
||
kamokuAccD &&
|
||
(kamokuAccD.account_code.startsWith("8") ||
|
||
kamokuAccD.account_code.startsWith("1") ||
|
||
kamokuAccD.account_code.startsWith("2") ||
|
||
kamokuAccD.account_code.startsWith("3") ||
|
||
kamokuAccD.account_code.startsWith("4"));
|
||
if (isExpense) {
|
||
document.querySelector(
|
||
'input[name="entryType"][value="expense"]',
|
||
).checked = true;
|
||
document.getElementById("entryAccountInput").value =
|
||
`${kamokuAccD.account_code} ${kamokuAccD.account_name}`;
|
||
document.getElementById(
|
||
"entryAccountInput",
|
||
).dataset.accountId = kamokuAccD.account_id;
|
||
const methodAcc = accounts.find(
|
||
(a) => a.account_id === mainCredit.account_id,
|
||
);
|
||
if (methodAcc) {
|
||
document.getElementById("entryMethodInput").value =
|
||
`${methodAcc.account_code} ${methodAcc.account_name}`;
|
||
document.getElementById(
|
||
"entryMethodInput",
|
||
).dataset.accountId = methodAcc.account_id;
|
||
}
|
||
document.getElementById("entryAmount").value = Number(
|
||
mainDebit.debit,
|
||
).toLocaleString();
|
||
} else {
|
||
document.querySelector(
|
||
'input[name="entryType"][value="income"]',
|
||
).checked = true;
|
||
const kamokuAccC = accounts.find(
|
||
(a) => a.account_id === mainCredit.account_id,
|
||
);
|
||
if (kamokuAccC) {
|
||
document.getElementById("entryAccountInput").value =
|
||
`${kamokuAccC.account_code} ${kamokuAccC.account_name}`;
|
||
document.getElementById(
|
||
"entryAccountInput",
|
||
).dataset.accountId = kamokuAccC.account_id;
|
||
}
|
||
document.getElementById("entryMethodInput").value =
|
||
`${kamokuAccD.account_code} ${kamokuAccD.account_name}`;
|
||
document.getElementById(
|
||
"entryMethodInput",
|
||
).dataset.accountId = kamokuAccD.account_id;
|
||
document.getElementById("entryAmount").value = Number(
|
||
mainDebit.debit,
|
||
).toLocaleString();
|
||
}
|
||
document.getElementById("entryTaxRate").value = "0";
|
||
document.getElementById("entryTaxAmount").value = "";
|
||
}
|
||
}
|
||
}
|
||
|
||
// ページ上部の仕訳入力フォームにスクロール
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
|
||
const dateInput = document.getElementById("entryDate");
|
||
dateInput.focus();
|
||
|
||
showAlert(
|
||
`仕訳を複製しました。\n\n※ このコピーは新規仕訳として独立した記録になります。\n※ 日付と摘要を必要に応じて修正してから登録してください。`,
|
||
"success",
|
||
);
|
||
} catch (error) {
|
||
console.error("Error:", error);
|
||
showAlert(`エラーが発生しました: ${error.message}`, "error");
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------
|
||
// 修正仕訳:逆仕訳を作って修正画面へ遷移
|
||
// --------------------------------------------------
|
||
async function reverseAndEdit(id) {
|
||
try {
|
||
// 获取元仕訳内容
|
||
const srcRes = await fetch(`${API}/journal-entries/${id}`);
|
||
if (!srcRes.ok) {
|
||
return;
|
||
}
|
||
|
||
const srcData = await srcRes.json();
|
||
localStorage.setItem("editSource", JSON.stringify(srcData));
|
||
|
||
// 打开修正画面
|
||
window.open("journal-edit.html", "_blank");
|
||
} catch (error) {
|
||
console.error("Error:", error);
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------
|
||
// 仕訳削除
|
||
// --------------------------------------------------
|
||
async function deleteJournalEntry(id) {
|
||
const reason = prompt("削除理由を入力してください:");
|
||
if (!reason || reason.trim() === "") {
|
||
showAlert("削除理由の入力が必要です", "warning");
|
||
return;
|
||
}
|
||
|
||
if (!confirm("この仕訳を削除してもよろしいですか?")) {
|
||
return;
|
||
}
|
||
|
||
const res = await fetch(`${API}/journal-entries/${id}`, {
|
||
method: "DELETE",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ deleted_reason: reason }),
|
||
});
|
||
|
||
const data = await res.json();
|
||
if (!res.ok) {
|
||
showAlert(data.detail || "削除に失敗しました", "error");
|
||
return;
|
||
}
|
||
|
||
showAlert("削除しました", "success");
|
||
searchJournals(); // リストを再読み込み
|
||
}
|
||
|
||
// ページロード後に検索条件を設定
|
||
window.addEventListener("DOMContentLoaded", async () => {
|
||
// デフォルト期間を設定(5/31決算:6月1日~現在)
|
||
const today = new Date();
|
||
const currentYear = today.getFullYear();
|
||
const currentMonth = today.getMonth() + 1; // 1-12
|
||
|
||
let fiscalStartYear;
|
||
if (currentMonth >= 6) {
|
||
// 6月以降:当年6月1日から
|
||
fiscalStartYear = currentYear;
|
||
} else {
|
||
// 1-5月:前年6月1日から
|
||
fiscalStartYear = currentYear - 1;
|
||
}
|
||
|
||
const defaultFromDate = `${fiscalStartYear}-06-01`;
|
||
const defaultToDate = today.toISOString().slice(0, 10);
|
||
|
||
// 前回の検索条件を復元
|
||
const savedConditions = localStorage.getItem("journalSearchConditions");
|
||
if (savedConditions) {
|
||
try {
|
||
const conditions = JSON.parse(savedConditions);
|
||
// 期間を復元
|
||
document.getElementById("searchFromDate").value =
|
||
conditions.fromDate || defaultFromDate;
|
||
document.getElementById("searchToDate").value =
|
||
conditions.toDate || defaultToDate;
|
||
|
||
// 摘要、科目、方向を復元
|
||
if (conditions.keyword !== undefined && conditions.keyword !== null)
|
||
document.getElementById("searchKeyword").value =
|
||
conditions.keyword;
|
||
if (
|
||
conditions.accountId !== undefined &&
|
||
conditions.accountId !== null
|
||
) {
|
||
const accountInput =
|
||
document.getElementById("searchAccountInput");
|
||
if (accountInput && accounts && accounts.length > 0) {
|
||
const account = accounts.find(
|
||
(a) => a.account_id === conditions.accountId,
|
||
);
|
||
if (account) {
|
||
accountInput.value = `${account.account_code} ${account.account_name}`;
|
||
accountInput.dataset.accountId = account.account_id;
|
||
}
|
||
}
|
||
}
|
||
if (
|
||
conditions.accountSide !== undefined &&
|
||
conditions.accountSide !== null
|
||
)
|
||
document.getElementById("searchAccountSide").value =
|
||
conditions.accountSide;
|
||
if (
|
||
conditions.includeHistory !== undefined &&
|
||
conditions.includeHistory !== null
|
||
)
|
||
document.getElementById("searchIncludeHistory").checked =
|
||
conditions.includeHistory;
|
||
} catch (e) {
|
||
// パース失敗時はデフォルト期間を設定
|
||
document.getElementById("searchFromDate").value = defaultFromDate;
|
||
document.getElementById("searchToDate").value = defaultToDate;
|
||
}
|
||
} else {
|
||
// 保存された条件がない場合はデフォルト期間を設定
|
||
document.getElementById("searchFromDate").value = defaultFromDate;
|
||
document.getElementById("searchToDate").value = defaultToDate;
|
||
}
|
||
|
||
// 検索条件が復元されたので、自動的に検索を実行
|
||
console.log("🔍 自動検索を実行します...");
|
||
await searchJournals();
|
||
});
|
||
</script>
|
||
<div
|
||
id="alertToastContainer"
|
||
style="
|
||
position: fixed;
|
||
top: 24px;
|
||
right: 24px;
|
||
z-index: 9999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
pointer-events: none;
|
||
"
|
||
></div>
|
||
<style>
|
||
@keyframes alertSlideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateX(40px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateX(0);
|
||
}
|
||
}
|
||
</style>
|
||
</body>
|
||
</html>
|