修正
This commit is contained in:
@@ -342,14 +342,20 @@
|
||||
|
||||
<div class="row">
|
||||
<label>摘要</label>
|
||||
<input
|
||||
type="text"
|
||||
id="description"
|
||||
list="descriptionHistory"
|
||||
style="flex: 3; min-width: 600px"
|
||||
placeholder="例:売上入金/経費支払 など"
|
||||
/>
|
||||
<datalist id="descriptionHistory"></datalist>
|
||||
<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">
|
||||
@@ -406,6 +412,14 @@
|
||||
<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">
|
||||
@@ -453,6 +467,257 @@
|
||||
|
||||
<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
|
||||
@@ -722,6 +987,8 @@
|
||||
.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 = "";
|
||||
@@ -730,8 +997,17 @@
|
||||
|
||||
// 税率を数値に変換(8r = 軽減8%)
|
||||
const rate = rateVal === "8r" ? 8 : Number(rateVal);
|
||||
// 税込金額から消費税額を計算(切上げ)
|
||||
const taxAmount = Math.ceil((amount * rate) / (100 + rate));
|
||||
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();
|
||||
}
|
||||
@@ -820,7 +1096,11 @@
|
||||
const filtered = accounts.filter((acc) => {
|
||||
const code = acc.account_code.toLowerCase();
|
||||
const name = acc.account_name.toLowerCase();
|
||||
return code.includes(searchText) || name.includes(searchText);
|
||||
return (
|
||||
code.includes(searchText) ||
|
||||
name.includes(searchText) ||
|
||||
`${code} ${name}`.includes(searchText)
|
||||
);
|
||||
});
|
||||
|
||||
renderDropdown(filtered, true);
|
||||
@@ -895,6 +1175,17 @@
|
||||
// 摘要履歴を読み込み
|
||||
loadDescriptionHistory();
|
||||
|
||||
// 摘要検索ドロップダウンをセットアップ
|
||||
setupDescriptionSearch();
|
||||
|
||||
// 詳細入力セクション初期化
|
||||
setupDetailDescriptionSearch();
|
||||
detailClearAll();
|
||||
// 今日の日付をデフォルト設定
|
||||
document.getElementById("detailEntryDate").value = new Date()
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
|
||||
console.log("✓ init() 完成");
|
||||
}
|
||||
|
||||
@@ -1006,7 +1297,11 @@
|
||||
const filtered = accounts.filter((acc) => {
|
||||
const code = acc.account_code.toLowerCase();
|
||||
const name = acc.account_name.toLowerCase();
|
||||
return code.includes(searchText) || name.includes(searchText);
|
||||
return (
|
||||
code.includes(searchText) ||
|
||||
name.includes(searchText) ||
|
||||
`${code} ${name}`.includes(searchText)
|
||||
);
|
||||
});
|
||||
|
||||
renderSearchDropdown(filtered, true);
|
||||
@@ -1043,17 +1338,7 @@
|
||||
// 摘要履歴管理
|
||||
// -----------------------------
|
||||
function loadDescriptionHistory() {
|
||||
const history = JSON.parse(
|
||||
localStorage.getItem("descriptionHistory") || "[]",
|
||||
);
|
||||
const datalist = document.getElementById("descriptionHistory");
|
||||
datalist.innerHTML = "";
|
||||
|
||||
history.forEach((desc) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = desc;
|
||||
datalist.appendChild(option);
|
||||
});
|
||||
// 旧datalist方式の互換用に残す(現在はカスタムドロップダウンで管理)
|
||||
}
|
||||
|
||||
function saveDescriptionToHistory(description) {
|
||||
@@ -1067,22 +1352,564 @@
|
||||
history = history.filter((d) => d !== description);
|
||||
history.unshift(description);
|
||||
|
||||
// 最大100件まで保存
|
||||
if (history.length > 100) {
|
||||
history = history.slice(0, 100);
|
||||
// 最大500件まで保存
|
||||
if (history.length > 500) {
|
||||
history = history.slice(0, 500);
|
||||
}
|
||||
|
||||
localStorage.setItem("descriptionHistory", JSON.stringify(history));
|
||||
loadDescriptionHistory();
|
||||
}
|
||||
|
||||
// 摘要検索ドロップダウン
|
||||
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) {
|
||||
alert(
|
||||
"日付が正しくありません。存在しない日付が入力されています(例:11月31日は無効です)",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!entryDate) {
|
||||
alert("仕訳日を入力してください");
|
||||
return;
|
||||
}
|
||||
if (!description) {
|
||||
alert("摘要を入力してください");
|
||||
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) {
|
||||
alert("科目が選択されていない行があります");
|
||||
valid = false;
|
||||
return;
|
||||
}
|
||||
if (debit > 0 && credit > 0) {
|
||||
alert("1行に借方と貸方の両方を入力することはできません");
|
||||
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) {
|
||||
alert("仕訳明細は2行以上必要です");
|
||||
return;
|
||||
}
|
||||
if (debitSum !== creditSum) {
|
||||
alert(
|
||||
`貸借が一致していません(差額:${Math.abs(debitSum - creditSum).toLocaleString()} 円)`,
|
||||
);
|
||||
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);
|
||||
alert("仕訳を登録しました");
|
||||
|
||||
const keep = document.getElementById("detailKeepAfterSubmit").checked;
|
||||
if (!keep) {
|
||||
detailClearAll();
|
||||
}
|
||||
// keep=true の場合は仕訳日・摘要・明細・金額すべてそのまま保持
|
||||
|
||||
searchJournals();
|
||||
} catch (e) {
|
||||
alert("登録エラー: " + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -1216,7 +2043,8 @@
|
||||
taxReceivedAccounts.length > 0
|
||||
? taxReceivedAccounts[0].account_id
|
||||
: null,
|
||||
rounding_mode: "floor",
|
||||
rounding_mode:
|
||||
document.getElementById("entryRoundingMode")?.value || "floor",
|
||||
};
|
||||
|
||||
console.log("送信するデータ:", JSON.stringify(payload, null, 2));
|
||||
|
||||
Reference in New Issue
Block a user