login画面追加

This commit is contained in:
admin
2026-02-07 14:25:28 +09:00
parent 9d1ad5594d
commit 331953bb7f
22 changed files with 763 additions and 135 deletions

View File

@@ -6,7 +6,6 @@ from fastapi.responses import JSONResponse, RedirectResponse
from pydantic import ValidationError
from fastapi.middleware.cors import CORSMiddleware
# FastAPI アプリケーション作成
app = FastAPI(
title="日本小規模企業向け会計システム"
@@ -32,13 +31,15 @@ async def validation_exception_handler(request: Request, exc: ValidationError):
# ─────────────────────────
# ルート(稼働確認)
# ─────────────────────────
#@app.get("/", include_in_schema=False)
#def root():
# return RedirectResponse(url="/index.html")
@app.get("/", include_in_schema=False)
def root():
return RedirectResponse("/login.html")
# ─────────────────────────
# ルーター登録(※ 必ず app 定義の後)
# ─────────────────────────
from app.modules.users.router import router as users_router
app.include_router(users_router)
from app.modules.accounts.router import router as accounts_router
app.include_router(accounts_router)
@@ -139,7 +140,7 @@ print("Frontend path =", frontend_path)
if frontend_path.exists():
app.mount(
"/",
StaticFiles(directory=frontend_path, html=True),
StaticFiles(directory=frontend_path),
name="frontend",
)
else:

View File

@@ -0,0 +1 @@
# Users Module

View File

@@ -0,0 +1,104 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
import hashlib
import os
from app.core.database import get_connection
router = APIRouter(prefix="/api/users", tags=["users"])
class LoginRequest(BaseModel):
username: str
password: str
class LoginResponse(BaseModel):
id: int
username: str
full_name: str | None
email: str | None
message: str
def hash_password(password: str) -> str:
"""パスワードをハッシュ化"""
# 実運用ではより安全なハッシュアルゴリズムを使用してください
return hashlib.sha256(password.encode()).hexdigest()
@router.post("/login", response_model=LoginResponse)
async def login(request: LoginRequest):
"""ユーザーのログイン処理"""
try:
conn = get_connection()
cur = conn.cursor()
# ユーザーが存在するか確認
cur.execute(
"SELECT id, username, password, full_name, email FROM users WHERE username = %s AND is_active = TRUE",
(request.username,)
)
user = cur.fetchone()
cur.close()
conn.close()
if not user:
raise HTTPException(status_code=401, detail="ユーザー名またはパスワードが正しくありません")
# パスワードの検証
hashed_password = hash_password(request.password)
if user["password"] != hashed_password:
raise HTTPException(status_code=401, detail="ユーザー名またはパスワードが正しくありません")
return LoginResponse(
id=user["id"],
username=user["username"],
full_name=user["full_name"],
email=user["email"],
message="ログインに成功しました"
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"ログイン処理エラー: {str(e)}")
@router.post("/register")
async def register(request: LoginRequest):
"""新規ユーザー登録"""
try:
if len(request.password) < 6:
raise HTTPException(status_code=400, detail="パスワードは6文字以上である必要があります")
conn = get_connection()
cur = conn.cursor()
# ユーザーが既に存在するか確認
cur.execute("SELECT id FROM users WHERE username = %s", (request.username,))
if cur.fetchone():
cur.close()
conn.close()
raise HTTPException(status_code=400, detail="このユーザー名は既に使用されています")
# ユーザーを登録
hashed_password = hash_password(request.password)
cur.execute(
"INSERT INTO users (username, password) VALUES (%s, %s) RETURNING id, username",
(request.username, hashed_password)
)
new_user = cur.fetchone()
conn.commit()
cur.close()
conn.close()
return {
"id": new_user["id"],
"username": new_user["username"],
"message": "ユーザー登録が完了しました"
}
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=f"登録処理エラー: {str(e)}")

View File

@@ -0,0 +1,14 @@
-- ユーザーテーブル作成
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
full_name VARCHAR(100),
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ユーザーの一覧表示用インデックス
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>科目マスタ</title>
<link rel="stylesheet" href="css/style.css" />
</head>

View File

@@ -1,13 +1,19 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>NJTS 会計システム - メイン</title>
<link rel="stylesheet" href="css/style.css" />
<script src="js/auth.js"></script>
<style>
body {
font-family: system-ui, -apple-system, "Yu Gothic UI",
"Hiragino Kaku Gothic ProN", "メイリオ", sans-serif;
font-family:
system-ui,
-apple-system,
"Yu Gothic UI",
"Hiragino Kaku Gothic ProN",
"メイリオ",
sans-serif;
padding: 24px;
}
.grid {
@@ -46,11 +52,34 @@
margin: 0;
font-size: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
font-size: 14px;
color: #333;
}
.logout-btn {
padding: 8px 12px;
background: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.logout-btn:hover {
background: #c82333;
}
</style>
</head>
<body>
<header>
<h1>NJTS</h1>
<div class="user-info">
<span>ユーザー: <strong id="username">読み込み中...</strong></span>
<button class="logout-btn" onclick="logout()">ログアウト</button>
</div>
</header>
<main class="grid">
@@ -78,6 +107,14 @@
</main>
<script>
// ユーザー情報を表示
document.addEventListener("DOMContentLoaded", function () {
const user = getCurrentUser();
if (user) {
document.getElementById("username").textContent = user.username;
}
});
// dropdown toggle
const toggle = document.getElementById("accountingToggle");
const menu = document.getElementById("accountingMenu");

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>修正仕訳入力</title>
<style>
body {
@@ -102,9 +103,8 @@
// ヘッダ反映
document.getElementById("entryDate").value = srcData.entry_date;
document.getElementById(
"description"
).value = `【修正後】${srcData.description}`;
document.getElementById("description").value =
`【修正後】${srcData.description}`;
// 明細反映
srcData.lines.forEach((l) => addRow(l));
@@ -126,7 +126,7 @@
${accounts
.map(
(a) =>
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`,
)
.join("")}
</select>

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>仕訳入力</title>
<style>
body {
@@ -393,12 +394,12 @@
// 仮払消費税等(あなたの DB では 505 は「未払消費税等」)
taxPaidAccounts = accounts.filter((a) =>
a.account_name.includes("仮払")
a.account_name.includes("仮払"),
);
// 仮受消費税等
taxReceivedAccounts = accounts.filter((a) =>
a.account_name.includes("仮受")
a.account_name.includes("仮受"),
);
setupTaxSelectors();
@@ -432,7 +433,7 @@
.addEventListener("change", () => {
// 端数処理変更時は全ての税行を再計算
const rows = document.querySelectorAll(
"#linesTable tbody tr:not(.tax-row)"
"#linesTable tbody tr:not(.tax-row)",
);
rows.forEach((row) => handleTax(row));
});
@@ -452,7 +453,7 @@
optgroup.label = group.label;
const groupAccounts = accounts.filter((a) =>
group.codes.includes(a.account_code)
group.codes.includes(a.account_code),
);
groupAccounts.forEach((acc) => {
@@ -471,7 +472,7 @@
// -----------------------------
function loadDescriptionHistory() {
const history = JSON.parse(
localStorage.getItem("descriptionHistory") || "[]"
localStorage.getItem("descriptionHistory") || "[]",
);
const datalist = document.getElementById("descriptionHistory");
datalist.innerHTML = "";
@@ -487,7 +488,7 @@
if (!description || description.trim() === "") return;
let history = JSON.parse(
localStorage.getItem("descriptionHistory") || "[]"
localStorage.getItem("descriptionHistory") || "[]",
);
// 重複を避け、先頭に追加
@@ -512,7 +513,7 @@
taxPaidAccounts
.map(
(a) =>
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`,
)
.join("");
@@ -521,7 +522,7 @@
taxReceivedAccounts
.map(
(a) =>
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`
`<option value="${a.account_id}">${a.account_code} ${a.account_name}</option>`,
)
.join("");
}
@@ -660,7 +661,7 @@
balanceStatus.style.background = "";
} else {
balanceStatus.textContent = `✗ 差額: ${Math.abs(
totalDebit - totalCredit
totalDebit - totalCredit,
).toLocaleString()}`;
balanceStatus.style.background = "#f8d7da";
balanceStatus.style.color = "#721c24";
@@ -677,10 +678,10 @@
const taxType = row.querySelector(".taxType").value;
const taxDir = row.querySelector(".taxDirection").value;
const debit = Number(
row.querySelector(".debit").value.replace(/,/g, "") || 0
row.querySelector(".debit").value.replace(/,/g, "") || 0,
);
const credit = Number(
row.querySelector(".credit").value.replace(/,/g, "") || 0
row.querySelector(".credit").value.replace(/,/g, "") || 0,
);
if (taxType === "none" || taxDir === "none") {
@@ -722,7 +723,7 @@
addRow(
true,
{ accountId: taxAcc, amount: taxAmount, side: "debit" },
parentId
parentId,
);
}
if (taxDir === "received") {
@@ -734,7 +735,7 @@
addRow(
true,
{ accountId: taxAcc, amount: taxAmount, side: "credit" },
parentId
parentId,
);
}
@@ -781,10 +782,10 @@
const accountId = Number(row.querySelector(".account").value);
const debit = Number(
row.querySelector(".debit").value.replace(/,/g, "") || 0
row.querySelector(".debit").value.replace(/,/g, "") || 0,
);
const credit = Number(
row.querySelector(".credit").value.replace(/,/g, "") || 0
row.querySelector(".credit").value.replace(/,/g, "") || 0,
);
const taxType = row.querySelector(".taxType").value;
const taxDirection = row.querySelector(".taxDirection").value;
@@ -818,7 +819,7 @@
// 税科目(仮払消費税等・仮受消費税等)が存在するかチェック
const hasTaxAccount = lines.some((line) => {
const account = accounts.find(
(a) => a.account_id === line.account_id
(a) => a.account_id === line.account_id,
);
return (
account &&
@@ -829,7 +830,7 @@
// 税科目が存在し、かつ税情報を持つ行がある場合は警告
const hasTaxInfo = lines.some(
(line) => line.tax_rate != null && line.tax_direction != null
(line) => line.tax_rate != null && line.tax_direction != null,
);
if (hasTaxAccount && hasTaxInfo) {
@@ -838,7 +839,7 @@
"警告:仮払消費税等・仮受消費税等の行がすでに存在していますが、他の行に税区分・税方向が設定されています。\n" +
"このまま登録すると税が二重計算され、借貸不平衡になります。\n\n" +
"税区分・税方向をすべて「対象外」に設定してから登録することをお勧めします。\n\n" +
"このまま登録しますか?"
"このまま登録しますか?",
)
) {
return;
@@ -903,7 +904,7 @@
} catch (error) {
console.error("送信エラー:", error);
alert(
`エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。`
`エラーが発生しました: ${error.message}\n\nサーバーが起動しているか確認してください。`,
);
}
}
@@ -955,7 +956,7 @@
try {
await callApi(
`/month-locks/unlock?fiscal_year=${year}&month=${month}`
`/month-locks/unlock?fiscal_year=${year}&month=${month}`,
);
el.innerText = `${year}${month}月 已解锁`;
await refreshMonthLocks(); // 可选:自动刷新列表
@@ -985,8 +986,8 @@
(x) =>
`${x.fiscal_year}-${String(x.month).padStart(
2,
"0"
)} locked_by=${x.locked_by ?? ""}`
"0",
)} locked_by=${x.locked_by ?? ""}`,
)
.join("\n");
} catch (e) {
@@ -1005,7 +1006,7 @@
const data = await res.json();
return (data || []).some(
(x) => x.fiscal_year === year && x.month === month && x.is_locked
(x) => x.fiscal_year === year && x.month === month && x.is_locked,
);
}
@@ -1049,7 +1050,7 @@
// 最近使った科目を表示
const recentAccountIds = JSON.parse(
localStorage.getItem("recentAccounts") || "[]"
localStorage.getItem("recentAccounts") || "[]",
);
if (recentAccountIds.length > 0) {
html += `<optgroup label="⭐ 最近使った科目">`;
@@ -1080,7 +1081,7 @@
// 未分组的其他科目
const groupedCodes = ACCOUNT_GROUPS.flatMap((g) => g.codes);
const others = accounts.filter(
(a) => !groupedCodes.includes(a.account_code)
(a) => !groupedCodes.includes(a.account_code),
);
if (others.length > 0) {
@@ -1101,7 +1102,7 @@
if (!accountId) return;
let recentAccounts = JSON.parse(
localStorage.getItem("recentAccounts") || "[]"
localStorage.getItem("recentAccounts") || "[]",
);
// 重複を除去して先頭に追加
@@ -1127,7 +1128,7 @@
const key = document.getElementById("searchKeyword").value;
const accountId = document.getElementById("searchAccountId").value;
const includeHistory = document.getElementById(
"searchIncludeHistory"
"searchIncludeHistory",
).checked;
const qs = new URLSearchParams();
@@ -1191,7 +1192,7 @@
{
year: "numeric",
month: "long",
}
},
);
const section = document.createElement("div");
@@ -1287,7 +1288,7 @@
const key = document.getElementById("searchKeyword").value;
const accountId = document.getElementById("searchAccountId").value;
const includeHistory = document.getElementById(
"searchIncludeHistory"
"searchIncludeHistory",
).checked;
// 検索条件を保存
@@ -1299,7 +1300,7 @@
keyword: key,
accountId: accountId,
includeHistory: includeHistory,
})
}),
);
const qs = new URLSearchParams();
@@ -1358,7 +1359,7 @@
{
year: "numeric",
month: "long",
}
},
);
const section = document.createElement("div");
@@ -1512,10 +1513,10 @@
console.log("コピーされたデータ:", data); // デバッグ用
console.log(
`${data.lines.length}行をコピーしました。税区分・税方向はすべて「対象外」に設定されています。`
`${data.lines.length}行をコピーしました。税区分・税方向はすべて「対象外」に設定されています。`,
);
alert(
`仕訳を複製しました(${data.lines.length}行)。\n税の自動計算を防ぐため、すべての行の税区分・税方向を「対象外」に設定しています。\n必要に応じて修正してから登録してください。`
`仕訳を複製しました(${data.lines.length}行)。\n税の自動計算を防ぐため、すべての行の税区分・税方向を「対象外」に設定しています。\n必要に応じて修正してから登録してください。`,
);
} catch (error) {
console.error("Error:", error);
@@ -1529,7 +1530,7 @@
async function reverseAndEdit(id) {
if (
!confirm(
"この仕訳の逆仕訳を作成し、修正仕訳入力画面を開きます。よろしいですか?"
"この仕訳の逆仕訳を作成し、修正仕訳入力画面を開きます。よろしいですか?",
)
)
return;
@@ -1543,14 +1544,14 @@
if (!res.ok) {
const data = await res.json().catch(() => ({}));
alert(
data.detail || `逆仕訳の作成に失敗しました (HTTP ${res.status})`
data.detail || `逆仕訳の作成に失敗しました (HTTP ${res.status})`,
);
return;
}
const data = await res.json();
alert(
`逆仕訳を作成しましたID: ${data.reversed_journal_entry_id}`
`逆仕訳を作成しましたID: ${data.reversed_journal_entry_id}`,
);
// ② 元仕訳内容を取得して修正画面へ遷移

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>仕訳一覧</title>
<style>
body {
@@ -63,7 +64,7 @@
fromDate: from,
toDate: to,
keyword: key,
})
}),
);
const qs = new URLSearchParams();
@@ -106,7 +107,7 @@
async function reverseAndEdit(id) {
if (
!confirm(
"この仕訳の逆仕訳を作成し、修正仕訳入力画面を開きます。よろしいですか?"
"この仕訳の逆仕訳を作成し、修正仕訳入力画面を開きます。よろしいですか?",
)
)
return;
@@ -120,14 +121,14 @@
if (!res.ok) {
const data = await res.json().catch(() => ({}));
alert(
data.detail || `逆仕訳の作成に失敗しました (HTTP ${res.status})`
data.detail || `逆仕訳の作成に失敗しました (HTTP ${res.status})`,
);
return;
}
const data = await res.json();
alert(
`逆仕訳を作成しましたID: ${data.reversed_journal_entry_id}`
`逆仕訳を作成しましたID: ${data.reversed_journal_entry_id}`,
);
// ② 元仕訳内容を取得して修正画面へ遷移

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>仕訳詳細</title>
<style>
body {

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>仕訳入力</title>
<link rel="stylesheet" href="css/style.css" />
</head>

41
frontend/js/auth.js Normal file
View File

@@ -0,0 +1,41 @@
/**
* 认证和会话管理
*/
// 检查用户是否已登录
function checkAuth() {
const user = localStorage.getItem("currentUser");
if (!user) {
// 用户未登录,重定向到登录页面
window.location.href = "/login.html";
return false;
}
return true;
}
// 获取当前登录的用户信息
function getCurrentUser() {
const user = localStorage.getItem("currentUser");
return user ? JSON.parse(user) : null;
}
// 保存用户信息(登录时使用)
function setCurrentUser(userData) {
localStorage.setItem("currentUser", JSON.stringify(userData));
}
// 注销用户
function logout() {
localStorage.removeItem("currentUser");
window.location.href = "/login.html";
}
// 在页面加载时检查认证(仅在非登录页面使用)
document.addEventListener("DOMContentLoaded", function () {
const currentPage = window.location.pathname;
// 排除登录页面的检查
if (!currentPage.includes("login.html")) {
checkAuth();
}
});

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>元帳</title>
<link rel="stylesheet" href="css/style.css" />
</head>

411
frontend/login.html Normal file
View File

@@ -0,0 +1,411 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NJTS 会計システム - ログイン</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family:
system-ui,
-apple-system,
"Yu Gothic UI",
"Hiragino Kaku Gothic ProN",
"メイリオ",
sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.login-container {
background: white;
border-radius: 10px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
width: 100%;
max-width: 400px;
padding: 40px;
}
.logo {
text-align: center;
margin-bottom: 30px;
}
.logo h1 {
font-size: 28px;
color: #333;
margin-bottom: 10px;
}
.logo p {
font-size: 14px;
color: #666;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: #333;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
transition: border-color 0.3s;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.button-group {
display: flex;
gap: 10px;
margin-top: 30px;
}
button {
flex: 1;
padding: 12px;
border: none;
border-radius: 5px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.btn-login {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
.btn-register {
background: transparent;
color: #667eea;
border: 2px solid #667eea;
}
.btn-register:hover {
background: #f0f0ff;
}
.message {
padding: 12px;
border-radius: 5px;
margin-bottom: 20px;
display: none;
font-size: 14px;
}
.message.error {
background: #fee;
color: #c33;
border: 1px solid #fcc;
}
.message.success {
background: #efe;
color: #3c3;
border: 1px solid #cfc;
}
.toggle-form {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: #666;
}
.toggle-form a {
color: #667eea;
text-decoration: none;
cursor: pointer;
font-weight: 500;
}
.toggle-form a:hover {
text-decoration: underline;
}
.form {
display: none;
}
.form.active {
display: block;
}
.loading {
display: none;
text-align: center;
color: #667eea;
margin-top: 10px;
}
</style>
</head>
<body>
<div class="login-container">
<div class="logo">
<h1>NJTS</h1>
<p>会計システム</p>
</div>
<!-- ログインフォーム -->
<div id="loginForm" class="form active">
<div id="messageLogin" class="message"></div>
<div class="form-group">
<label for="loginUsername">ユーザー名</label>
<input
type="text"
id="loginUsername"
placeholder="ユーザー名を入力"
required
/>
</div>
<div class="form-group">
<label for="loginPassword">パスワード</label>
<input
type="password"
id="loginPassword"
placeholder="パスワードを入力"
required
/>
</div>
<div class="button-group">
<button class="btn-login" onclick="handleLogin()">ログイン</button>
</div>
<div class="toggle-form">
新規登録?
<a onclick="toggleForm()">アカウント作成</a>
</div>
<div class="loading" id="loginLoading">処理中...</div>
</div>
<!-- 登録フォーム -->
<div id="registerForm" class="form">
<div id="messageRegister" class="message"></div>
<div class="form-group">
<label for="regUsername">ユーザー名</label>
<input
type="text"
id="regUsername"
placeholder="ユーザー名を入力"
required
/>
</div>
<div class="form-group">
<label for="regPassword">パスワード</label>
<input
type="password"
id="regPassword"
placeholder="6文字以上で設定"
required
/>
</div>
<div class="button-group">
<button class="btn-login" onclick="handleRegister()">登録</button>
<button class="btn-register" onclick="toggleForm()">
キャンセル
</button>
</div>
<div class="loading" id="registerLoading">処理中...</div>
</div>
</div>
<script>
function toggleForm() {
const loginForm = document.getElementById("loginForm");
const registerForm = document.getElementById("registerForm");
loginForm.classList.toggle("active");
registerForm.classList.toggle("active");
// メッセージをリセット
clearMessage("messageLogin");
clearMessage("messageRegister");
}
function showMessage(elementId, message, isError = false) {
const messageEl = document.getElementById(elementId);
messageEl.textContent = message;
messageEl.className = `message ${isError ? "error" : "success"}`;
messageEl.style.display = "block";
}
function clearMessage(elementId) {
const messageEl = document.getElementById(elementId);
messageEl.style.display = "none";
messageEl.textContent = "";
}
async function handleLogin() {
const username = document.getElementById("loginUsername").value;
const password = document.getElementById("loginPassword").value;
if (!username || !password) {
showMessage(
"messageLogin",
"ユーザー名とパスワードを入力してください",
true,
);
return;
}
const loadingEl = document.getElementById("loginLoading");
loadingEl.style.display = "block";
try {
const response = await fetch("/api/users/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (response.ok) {
// ユーザー情報をlocalStorageに保存
localStorage.setItem("currentUser", JSON.stringify(data));
showMessage("messageLogin", "ログインに成功しました!", false);
// 2秒後にメインページにリダイレクト
setTimeout(() => {
window.location.href = "/index.html";
}, 2000);
} else {
showMessage(
"messageLogin",
data.detail || "ログインに失敗しました",
true,
);
}
} catch (error) {
showMessage(
"messageLogin",
"エラーが発生しました: " + error.message,
true,
);
} finally {
loadingEl.style.display = "none";
}
}
async function handleRegister() {
const username = document.getElementById("regUsername").value;
const password = document.getElementById("regPassword").value;
if (!username || !password) {
showMessage(
"messageRegister",
"ユーザー名とパスワードを入力してください",
true,
);
return;
}
if (password.length < 6) {
showMessage(
"messageRegister",
"パスワードは6文字以上である必要があります",
true,
);
return;
}
const loadingEl = document.getElementById("registerLoading");
loadingEl.style.display = "block";
try {
const response = await fetch("/api/users/register", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (response.ok) {
showMessage(
"messageRegister",
"登録が完了しました。ログインしてください",
false,
);
setTimeout(() => {
toggleForm();
document.getElementById("loginUsername").value = username;
document.getElementById("loginPassword").value = "";
}, 2000);
} else {
showMessage(
"messageRegister",
data.detail || "登録に失敗しました",
true,
);
}
} catch (error) {
showMessage(
"messageRegister",
"エラーが発生しました: " + error.message,
true,
);
} finally {
loadingEl.style.display = "none";
}
}
// Enterキーでログイン/登録
document.addEventListener("keypress", (e) => {
if (e.key === "Enter") {
const loginForm = document.getElementById("loginForm");
if (loginForm.classList.contains("active")) {
handleLogin();
} else {
handleRegister();
}
}
});
</script>
</body>
</html>

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>期首残高入力</title>
<link rel="stylesheet" href="css/style.css" />
</head>

View File

@@ -2,6 +2,7 @@
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 月次給与計算</title>
<link rel="stylesheet" href="/css/style.css" />

View File

@@ -2,6 +2,7 @@
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 従業員管理</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 給与設定</title>
<link rel="stylesheet" href="/css/style.css" />
@@ -213,7 +214,7 @@
async function loadEmployees() {
try {
const response = await fetch(
`${API_BASE}/payroll/employees/?is_active=true`
`${API_BASE}/payroll/employees/?is_active=true`,
);
employees = await response.json();
@@ -223,7 +224,7 @@
employees
.map(
(emp) =>
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`
`<option value="${emp.employee_id}">${emp.employee_code} - ${emp.name}</option>`,
)
.join("");
@@ -241,7 +242,7 @@
for (const emp of employees) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/salary/${emp.employee_id}/current`
`${API_BASE}/payroll/settings/salary/${emp.employee_id}/current`,
);
if (response.ok) {
const setting = await response.json();
@@ -255,7 +256,7 @@
} catch (error) {
console.error(
`従業員 ${emp.employee_id} の給与設定取得失敗`,
error
error,
);
}
}
@@ -316,7 +317,7 @@
})" style="padding: 5px 10px; font-size: 12px;">編集</button>
</td>
</tr>
`
`,
)
.join("")
: '<tr><td colspan="11" style="text-align:center;">給与設定が登録されていません</td></tr>'
@@ -334,7 +335,7 @@
// 全従業員の設定を取得して該当するものを探す
for (const emp of employees) {
const response = await fetch(
`${API_BASE}/payroll/settings/salary/${emp.employee_id}`
`${API_BASE}/payroll/settings/salary/${emp.employee_id}`,
);
const settings = await response.json();
const setting = settings.find((s) => s.setting_id === settingId);
@@ -408,7 +409,7 @@
commute_allowance: parseFloat(formData.get("commute_allowance")) || 0,
other_allowance: parseFloat(formData.get("other_allowance")) || 0,
employment_insurance_eligible: document.getElementById(
"employment_insurance_eligible"
"employment_insurance_eligible",
).checked,
valid_from: formData.get("valid_from"),
};
@@ -431,7 +432,7 @@
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
},
);
} else {
// 新規追加
@@ -444,7 +445,7 @@
if (response.ok) {
alert(
settingId ? "給与設定を更新しました" : "給与設定を保存しました"
settingId ? "給与設定を更新しました" : "給与設定を保存しました",
);
hideAddForm();
loadAllSalarySettings();

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理 - 設定</title>
<link rel="stylesheet" href="/css/style.css" />
@@ -651,7 +652,7 @@
async function loadStdRemunerationYears() {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/years`
`${API_BASE}/payroll/settings/standard-remuneration/years`,
);
const years = await response.json();
@@ -678,7 +679,7 @@
<div id="std-table-${year}">読み込み中...</div>
</div>
</div>
`
`,
)
.join("");
@@ -729,7 +730,7 @@
// セッションストレージからメタデータを取得
const metadata = JSON.parse(
sessionStorage.getItem("stdRemunMetadata") || "{}"
sessionStorage.getItem("stdRemunMetadata") || "{}",
);
console.log("取得したメタデータ:", metadata);
@@ -752,9 +753,8 @@
document.getElementById(`std-table-${year}`).innerHTML = tablesHtml;
} catch (error) {
console.error("データ読み込みエラー:", error);
document.getElementById(
`std-table-${year}`
).innerHTML = `<p style="color:red;">データの取得に失敗しました: ${error.message}</p>`;
document.getElementById(`std-table-${year}`).innerHTML =
`<p style="color:red;">データの取得に失敗しました: ${error.message}</p>`;
}
}
@@ -768,7 +768,7 @@
console.log("renderStdRemunerationTable - metadata内容:", metadata);
console.log(
"renderStdRemunerationTable - metadata.source:",
metadata.source
metadata.source,
);
if (metadata.source) {
sourceHtml = `<p style="margin-bottom: 15px; padding: 10px; background: #f0f8ff; border-left: 3px solid #0066cc; color: #333;"><strong>対象期間:</strong> ${metadata.source}</p>`;
@@ -831,7 +831,7 @@
step1Data.map((d) => ({
grade: d.grade,
pension: d.pension_insurance,
}))
})),
);
// ステップ2: 非ゼロ値を下方へ延続前の有効値が0以下になったら、前の非ゼロ値を使用
@@ -856,7 +856,7 @@
adjustedData.map((d) => ({
grade: d.grade,
pension: d.pension_insurance,
}))
})),
);
console.log("調整後のデータ最初の3件:", adjustedData.slice(0, 3));
@@ -927,15 +927,15 @@
<td>${Number(row.salary_from ?? 0).toLocaleString()}</td>
<td>${Number(row.salary_to ?? 0).toLocaleString()}</td>
<td>${Number(
row.health_insurance_no_care ?? 0
row.health_insurance_no_care ?? 0,
).toLocaleString()}</td>
<td>${Number(healthNoCareHalf ?? 0).toLocaleString()}</td>
<td>${Number(
row.health_insurance_with_care ?? 0
row.health_insurance_with_care ?? 0,
).toLocaleString()}</td>
<td>${Number(healthWithCareHalf ?? 0).toLocaleString()}</td>
<td>${Number(
row.pension_insurance ?? 0
row.pension_insurance ?? 0,
).toLocaleString()}</td>
<td>${Number(pensionHalf ?? 0).toLocaleString()}</td>
</tr>
@@ -958,7 +958,7 @@
async function deleteStdRemunerationYear(year) {
const confirmed = confirm(
`⚠️ 警告\n\n${year}年度の標準報酬月額表データを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?`
`⚠️ 警告\n\n${year}年度の標準報酬月額表データを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?`,
);
if (!confirmed) {
console.log(`${year}年度の削除がキャンセルされました`);
@@ -970,7 +970,7 @@
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/year/${year}`,
{ method: "DELETE" }
{ method: "DELETE" },
);
console.log("削除レスポンスステータス:", response.status);
@@ -979,7 +979,7 @@
if (response.ok) {
alert(
`${year}年度のデータ(${result.deleted_count}件)を削除しました`
`${year}年度のデータ(${result.deleted_count}件)を削除しました`,
);
console.log(`=== ${year}年度データ削除完了 ===`);
@@ -987,7 +987,7 @@
await loadStdRemunerationYears();
} else {
alert(
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
"削除に失敗しました:\n" + JSON.stringify(result.detail || result),
);
}
} catch (error) {
@@ -998,7 +998,7 @@
async function clearAllStdRemunerationData() {
const confirmed = confirm(
"⚠️ 警告\n\n標準報酬月額表のすべてのデータを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか"
"⚠️ 警告\n\n標準報酬月額表のすべてのデータを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか",
);
if (!confirmed) {
console.log("削除操作がキャンセルされました");
@@ -1010,7 +1010,7 @@
try {
const response = await fetch(
`${API_BASE}/payroll/settings/standard-remuneration/all`,
{ method: "DELETE" }
{ method: "DELETE" },
);
console.log("削除レスポンスステータス:", response.status);
@@ -1019,7 +1019,7 @@
if (response.ok) {
alert(
`${result.deleted_count}件のデータを削除しました\n\n再度Excelファイルをインポートしてください`
`${result.deleted_count}件のデータを削除しました\n\n再度Excelファイルをインポートしてください`,
);
console.log("=== 全データ削除完了 ===");
@@ -1028,7 +1028,7 @@
"<p>データが削除されました。Excelファイルを再度インポートしてください。</p>";
} else {
alert(
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
"削除に失敗しました:\n" + JSON.stringify(result.detail || result),
);
}
} catch (error) {
@@ -1150,7 +1150,7 @@
async function editInsuranceRate(rateId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-rates`
`${API_BASE}/payroll/settings/insurance-rates`,
);
const rates = await response.json();
const rate = rates.find((r) => r.rate_id === rateId);
@@ -1184,7 +1184,7 @@
async function copyInsuranceRate(rateId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-rates`
`${API_BASE}/payroll/settings/insurance-rates`,
);
const rates = await response.json();
const rate = rates.find((r) => r.rate_id === rateId);
@@ -1265,7 +1265,7 @@
// 介護保険年齢を数値に変換空の場合はnull
if (data.care_insurance_age_threshold) {
data.care_insurance_age_threshold = parseInt(
data.care_insurance_age_threshold
data.care_insurance_age_threshold,
);
} else {
data.care_insurance_age_threshold = null;
@@ -1278,7 +1278,7 @@
"送信するURL:",
isEditMode
? `${API_BASE}/payroll/settings/insurance-rates/${rateId}`
: `${API_BASE}/payroll/settings/insurance-rates`
: `${API_BASE}/payroll/settings/insurance-rates`,
);
console.log("送信するデータ:", JSON.stringify(data, null, 2));
@@ -1302,7 +1302,7 @@
const result = await response.json();
console.log("成功レスポンス:", result);
alert(
isEditMode ? "保険料率を更新しました" : "保険料率を登録しました"
isEditMode ? "保険料率を更新しました" : "保険料率を登録しました",
);
hideInsuranceForm();
loadInsuranceRates();
@@ -1312,7 +1312,7 @@
alert(
(isEditMode ? "更新" : "登録") +
"に失敗しました: " +
JSON.stringify(error.detail)
JSON.stringify(error.detail),
);
}
} catch (error) {
@@ -1328,7 +1328,7 @@
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-rates/${rateId}`,
{ method: "DELETE" }
{ method: "DELETE" },
);
if (response.ok) {
@@ -1362,7 +1362,7 @@
async function loadChildSupportRates() {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/child-support-rates`
`${API_BASE}/payroll/settings/child-support-rates`,
);
const rates = await response.json();
@@ -1390,10 +1390,10 @@
<tr>
<td>${rate.rate_year}</td>
<td>¥${Number(
rate.income_threshold
rate.income_threshold,
).toLocaleString()}</td>
<td>${(parseFloat(rate.contribution_rate) * 100).toFixed(
3
3,
)}%</td>
<td>${rate.notes || "-"}</td>
<td>
@@ -1405,7 +1405,7 @@
})">削除</button>
</td>
</tr>
`
`,
)
.join("")}
</tbody>
@@ -1422,7 +1422,7 @@
async function editChildSupportRate(contributionId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/child-support-rates`
`${API_BASE}/payroll/settings/child-support-rates`,
);
const rates = await response.json();
const rate = rates.find((r) => r.contribution_id === contributionId);
@@ -1493,7 +1493,7 @@
alert(
isEditMode
? "子ども・子育て拠出金率を更新しました"
: "子ども・子育て拠出金率を登録しました"
: "子ども・子育て拠出金率を登録しました",
);
hideChildSupportForm();
loadChildSupportRates();
@@ -1502,7 +1502,7 @@
alert(
(isEditMode ? "更新" : "登録") +
"に失敗しました: " +
JSON.stringify(error.detail)
JSON.stringify(error.detail),
);
}
} catch (error) {
@@ -1517,7 +1517,7 @@
try {
const response = await fetch(
`${API_BASE}/payroll/settings/child-support-rates/${contributionId}`,
{ method: "DELETE" }
{ method: "DELETE" },
);
if (response.ok) {
@@ -1550,7 +1550,7 @@
async function loadLimitSettings() {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-limits`
`${API_BASE}/payroll/settings/insurance-limits`,
);
const limits = await response.json();
@@ -1591,7 +1591,7 @@
})">削除</button>
</td>
</tr>
`
`,
)
.join("")}
</tbody>
@@ -1608,7 +1608,7 @@
async function editLimitSetting(limitId) {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-limits`
`${API_BASE}/payroll/settings/insurance-limits`,
);
const limits = await response.json();
const limit = limits.find((l) => l.limit_id === limitId);
@@ -1676,7 +1676,7 @@
if (response.ok) {
alert(
isEditMode ? "上限設定を更新しました" : "上限設定を登録しました"
isEditMode ? "上限設定を更新しました" : "上限設定を登録しました",
);
hideLimitForm();
loadLimitSettings();
@@ -1685,7 +1685,7 @@
alert(
(isEditMode ? "更新" : "登録") +
"に失敗しました: " +
JSON.stringify(error.detail)
JSON.stringify(error.detail),
);
}
} catch (error) {
@@ -1700,7 +1700,7 @@
try {
const response = await fetch(
`${API_BASE}/payroll/settings/insurance-limits/${limitId}`,
{ method: "DELETE" }
{ method: "DELETE" },
);
if (response.ok) {
@@ -1732,7 +1732,7 @@
async function loadIncomeTaxYears() {
try {
const response = await fetch(
`${API_BASE}/payroll/settings/income-tax/years`
`${API_BASE}/payroll/settings/income-tax/years`,
);
const data = await response.json();
@@ -1772,7 +1772,7 @@
<div id="table-${year}">読み込み中...</div>
</div>
</div>
`
`,
)
.join("");
@@ -1785,7 +1785,7 @@
async function deleteTaxDataYear(year) {
const confirmed = confirm(
`⚠️ 警告\n\n${year}年度の所得税率表データを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?`
`⚠️ 警告\n\n${year}年度の所得税率表データを削除します。\nこの操作は取り消せません。\n\n本当に削除してもよろしいですか?`,
);
if (!confirmed) {
console.log(`${year}年度の削除がキャンセルされました`);
@@ -1797,7 +1797,7 @@
try {
const response = await fetch(
`${API_BASE}/payroll/settings/income-tax/year/${year}`,
{ method: "DELETE" }
{ method: "DELETE" },
);
console.log("削除レスポンスステータス:", response.status);
@@ -1806,7 +1806,7 @@
if (response.ok) {
alert(
`${year}年度のデータ(${result.deleted_count}件)を削除しました`
`${year}年度のデータ(${result.deleted_count}件)を削除しました`,
);
console.log(`=== ${year}年度所得税データ削除完了 ===`);
@@ -1814,7 +1814,7 @@
await loadIncomeTaxYears();
} else {
alert(
"削除に失敗しました:\n" + JSON.stringify(result.detail || result)
"削除に失敗しました:\n" + JSON.stringify(result.detail || result),
);
}
} catch (error) {
@@ -1851,7 +1851,7 @@
try {
const response = await fetch(
`${API_BASE}/payroll/settings/income-tax?${params}`
`${API_BASE}/payroll/settings/income-tax?${params}`,
);
const taxes = await response.json();
@@ -1879,13 +1879,13 @@
<tr>
<td>${tax.dependents_count}人</td>
<td>¥${Number(
tax.monthly_income_from
tax.monthly_income_from,
).toLocaleString()}</td>
<td>¥${Number(tax.monthly_income_to).toLocaleString()}</td>
<td>¥${Number(tax.tax_amount).toLocaleString()}</td>
<td>${tax.valid_from} ${tax.valid_to || "現在"}</td>
</tr>
`
`,
)
.join("")}
</tbody>
@@ -1908,7 +1908,7 @@
const taxYear = prompt(
"税年度を入力してください(空欄の場合はファイルから自動抽出):",
""
"",
);
try {
@@ -1948,7 +1948,7 @@
// 都道府県をプロンプトで取得
const prefecture = prompt(
"都道府県を入力してください(例: 東京、神奈川):",
""
"",
);
if (!prefecture || !prefecture.trim()) {
alert("都道府県を入力してください");
@@ -1970,7 +1970,7 @@
{
method: "POST",
body: formData,
}
},
);
console.log("レスポンスステータス:", response.status);
@@ -1990,7 +1990,7 @@
console.log("メタデータ保存:", result.metadata);
sessionStorage.setItem(
"stdRemunMetadata",
JSON.stringify(result.metadata)
JSON.stringify(result.metadata),
);
}
@@ -2009,7 +2009,7 @@
} else if (typeof errorDetail === "object") {
errorMessage += JSON.stringify(errorDetail, null, 2).substring(
0,
1000
1000,
);
} else {
errorMessage += String(errorDetail).substring(0, 500);

View File

@@ -1,7 +1,8 @@
<!DOCTYPE html>
<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>給与管理システム</title>
<link rel="stylesheet" href="/css/style.css" />
@@ -60,7 +61,10 @@
<p>従業員の給与計算、扶養管理、保険料・税金の管理を行います</p>
<div class="menu-grid">
<div class="menu-card" onclick="location.href='payroll-employees.html'">
<div
class="menu-card"
onclick="location.href = 'payroll-employees.html'"
>
<div class="icon">👥</div>
<h2>従業員管理</h2>
<p>従業員の基本情報、扶養家族情報を管理します</p>
@@ -84,7 +88,10 @@
<p>毎月の給与計算、給与明細の作成・承認を行います</p>
</div>
<div class="menu-card" onclick="location.href='payroll-settings.html'">
<div
class="menu-card"
onclick="location.href = 'payroll-settings.html'"
>
<div class="icon">⚙️</div>
<h2>税率・保険料設定</h2>
<p>社会保険料率、所得税率表の管理を行います</p>

View File

@@ -2,6 +2,7 @@
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>試算表</title>
<style>
body {

View File

@@ -2,6 +2,7 @@
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="js/auth.js"></script>
<title>账票出力 - UI テスト</title>
<style>
body {