diff --git a/backend/app/main.py b/backend/app/main.py index b7f2a06..0414da1 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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: diff --git a/backend/app/modules/users/__init__.py b/backend/app/modules/users/__init__.py new file mode 100644 index 0000000..a9e9188 --- /dev/null +++ b/backend/app/modules/users/__init__.py @@ -0,0 +1 @@ +# Users Module diff --git a/backend/app/modules/users/router.py b/backend/app/modules/users/router.py new file mode 100644 index 0000000..2633201 --- /dev/null +++ b/backend/app/modules/users/router.py @@ -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)}") diff --git a/backend/sql/create_users_table.sql b/backend/sql/create_users_table.sql new file mode 100644 index 0000000..dcad310 --- /dev/null +++ b/backend/sql/create_users_table.sql @@ -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); diff --git a/frontend/accounts.html b/frontend/accounts.html index 0b43cfb..4bdc632 100644 --- a/frontend/accounts.html +++ b/frontend/accounts.html @@ -1,7 +1,8 @@ - + + 科目マスタ diff --git a/frontend/index.html b/frontend/index.html index 1965dfb..afeb497 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,13 +1,19 @@ - + NJTS 会計システム - メイン +

NJTS

+
+ ユーザー: 読み込み中... + +
@@ -78,6 +107,14 @@
修正仕訳入力 + + + +
+ + + +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ 新規登録? + アカウント作成 +
+ +
処理中...
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
処理中...
+
+
+ + + + diff --git a/frontend/opening_balance.html b/frontend/opening_balance.html index f13279a..52c2838 100644 --- a/frontend/opening_balance.html +++ b/frontend/opening_balance.html @@ -1,7 +1,8 @@ - + + 期首残高入力 @@ -39,7 +40,7 @@
- + diff --git a/frontend/payroll-calculation.html b/frontend/payroll-calculation.html index 7e21560..2c95485 100644 --- a/frontend/payroll-calculation.html +++ b/frontend/payroll-calculation.html @@ -2,6 +2,7 @@ + 給与管理 - 月次給与計算 diff --git a/frontend/payroll-employees.html b/frontend/payroll-employees.html index 7c2229b..ace0574 100644 --- a/frontend/payroll-employees.html +++ b/frontend/payroll-employees.html @@ -2,6 +2,7 @@ + 給与管理 - 従業員管理 diff --git a/frontend/payroll-salary-settings.html b/frontend/payroll-salary-settings.html index 4dbdba7..17c6d1a 100644 --- a/frontend/payroll-salary-settings.html +++ b/frontend/payroll-salary-settings.html @@ -1,7 +1,8 @@ - + + 給与管理 - 給与設定 @@ -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) => - `` + ``, ) .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;">編集 - ` + `, ) .join("") : '給与設定が登録されていません' @@ -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(); diff --git a/frontend/payroll-settings.html b/frontend/payroll-settings.html index da76445..4505611 100644 --- a/frontend/payroll-settings.html +++ b/frontend/payroll-settings.html @@ -1,7 +1,8 @@ - + + 給与管理 - 設定 @@ -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 @@
読み込み中...
- ` + `, ) .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 = `

データの取得に失敗しました: ${error.message}

`; + document.getElementById(`std-table-${year}`).innerHTML = + `

データの取得に失敗しました: ${error.message}

`; } } @@ -768,7 +768,7 @@ console.log("renderStdRemunerationTable - metadata内容:", metadata); console.log( "renderStdRemunerationTable - metadata.source:", - metadata.source + metadata.source, ); if (metadata.source) { sourceHtml = `

対象期間: ${metadata.source}

`; @@ -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 @@ ${Number(row.salary_from ?? 0).toLocaleString()} ${Number(row.salary_to ?? 0).toLocaleString()} ${Number( - row.health_insurance_no_care ?? 0 + row.health_insurance_no_care ?? 0, ).toLocaleString()} ${Number(healthNoCareHalf ?? 0).toLocaleString()} ${Number( - row.health_insurance_with_care ?? 0 + row.health_insurance_with_care ?? 0, ).toLocaleString()} ${Number(healthWithCareHalf ?? 0).toLocaleString()} ${Number( - row.pension_insurance ?? 0 + row.pension_insurance ?? 0, ).toLocaleString()} ${Number(pensionHalf ?? 0).toLocaleString()} @@ -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 @@ "

データが削除されました。Excelファイルを再度インポートしてください。

"; } 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 @@ ${rate.rate_year} ¥${Number( - rate.income_threshold + rate.income_threshold, ).toLocaleString()} ${(parseFloat(rate.contribution_rate) * 100).toFixed( - 3 + 3, )}% ${rate.notes || "-"} @@ -1405,7 +1405,7 @@ })">削除 - ` + `, ) .join("")} @@ -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(); @@ -1579,8 +1579,8 @@ ${limit.setting_type} ¥${Number(limit.limit_amount).toLocaleString()} ${limit.valid_from} ~ ${ - limit.valid_to || "現在" - } + limit.valid_to || "現在" + } ${limit.notes || "-"} - ` + `, ) .join("")} @@ -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 @@
読み込み中...
- ` + `, ) .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 @@ ${tax.dependents_count}人 ¥${Number( - tax.monthly_income_from + tax.monthly_income_from, ).toLocaleString()} ¥${Number(tax.monthly_income_to).toLocaleString()} ¥${Number(tax.tax_amount).toLocaleString()} ${tax.valid_from} ~ ${tax.valid_to || "現在"} - ` + `, ) .join("")} @@ -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); diff --git a/frontend/payroll.html b/frontend/payroll.html index 606af90..d4ad1de 100644 --- a/frontend/payroll.html +++ b/frontend/payroll.html @@ -1,7 +1,8 @@ - + + 給与管理システム @@ -60,7 +61,10 @@

従業員の給与計算、扶養管理、保険料・税金の管理を行います