diff --git a/_check_compose.py b/_check_compose.py new file mode 100644 index 0000000..afc4d55 --- /dev/null +++ b/_check_compose.py @@ -0,0 +1,16 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) + +# NAS上の実際のdocker-compose.ymlを確認 +_, o, _ = c.exec_command('cat /volume1/docker/njts-accounting/docker-compose.yml') +print("=== docker-compose.yml ===") +print(o.read().decode()) + +# コンテナの全マウントを確認 +_, o2, _ = c.exec_command('docker inspect njts-accounting-backend --format "{{range .Mounts}}{{.Source}} -> {{.Destination}}{{println}}{{end}}"') +print("=== マウント一覧 ===") +print(o2.read().decode()) + +c.close() diff --git a/_check_containers.py b/_check_containers.py new file mode 100644 index 0000000..70ebaed --- /dev/null +++ b/_check_containers.py @@ -0,0 +1,7 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) +_, o, _ = c.exec_command('cd /volume1/docker/njts-accounting && docker compose ps') +print(o.read().decode()) +c.close() diff --git a/_check_mount.py b/_check_mount.py new file mode 100644 index 0000000..fcf5e48 --- /dev/null +++ b/_check_mount.py @@ -0,0 +1,15 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) +# コンテナ内の /app/frontend の内容確認 +_, o, _ = c.exec_command('docker exec njts-accounting-backend ls -la /app/frontend/ | head -20') +print("=== コンテナ内 /app/frontend ===") +print(o.read().decode()) +# コンテナのマウント情報 +_, o2, _ = c.exec_command('docker inspect njts-accounting-backend --format "{{json .Mounts}}"') +import json +mounts = json.loads(o2.read().decode()) +for m in mounts: + print(f" {m.get('Source')} -> {m.get('Destination')}") +c.close() diff --git a/_check_nas_index.py b/_check_nas_index.py new file mode 100644 index 0000000..bea733e --- /dev/null +++ b/_check_nas_index.py @@ -0,0 +1,10 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) +_, o, _ = c.exec_command('grep -n "bank-statement" /volume1/docker/njts-accounting/frontend/index.html') +result = o.read().decode() +print("NAS index.html grep結果:", repr(result)) +_, o2, _ = c.exec_command('wc -l /volume1/docker/njts-accounting/frontend/index.html') +print("行数:", o2.read().decode().strip()) +c.close() diff --git a/_check_nas_index2.py b/_check_nas_index2.py new file mode 100644 index 0000000..1932bd8 --- /dev/null +++ b/_check_nas_index2.py @@ -0,0 +1,7 @@ +import paramiko +c = paramiko.SSHClient() +c.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +c.connect('192.168.0.61', username='root', password='59911784', timeout=15) +_, o, _ = c.exec_command('sed -n "187,205p" /volume1/docker/njts-accounting/frontend/index.html') +print(o.read().decode()) +c.close() diff --git a/backend/app/db_auto_migration.py b/backend/app/db_auto_migration.py index 92741d8..1130d78 100644 --- a/backend/app/db_auto_migration.py +++ b/backend/app/db_auto_migration.py @@ -146,6 +146,13 @@ def run_auto_migration(): except Exception as e: print(f"⚠️ ねんきんポータル テーブル作成エラー(無視): {e}") + # ── 銀行明細テーブル作成 ── + try: + from app.modules.bank_statements.migration import create_bank_statement_table + create_bank_statement_table() + except Exception as e: + print(f"⚠️ 銀行明細テーブル作成エラー(無視): {e}") + except Exception as e: print(f"\n❌ 無法連接到數據庫或執行遷移: {e}") print("\n提示:") diff --git a/backend/app/main.py b/backend/app/main.py index e848bfe..94e789c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -126,6 +126,9 @@ app.include_router(file_upload.router) from app.modules.egov.router import router as egov_router app.include_router(egov_router) +from app.modules.bank_statements.router import router as bank_statements_router +app.include_router(bank_statements_router) + # ───────────────────────── # 給与モジュール (Payroll Module) # ───────────────────────── diff --git a/backend/app/modules/bank_statements/__init__.py b/backend/app/modules/bank_statements/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/modules/bank_statements/migration.py b/backend/app/modules/bank_statements/migration.py new file mode 100644 index 0000000..a79268c --- /dev/null +++ b/backend/app/modules/bank_statements/migration.py @@ -0,0 +1,41 @@ +"""銀行明細テーブルのマイグレーション""" +import os +from psycopg import connect + + +def create_bank_statement_table(): + conn = connect( + host=os.getenv("DB_HOST"), + port=int(os.getenv("DB_PORT", 5432)), + dbname=os.getenv("DB_NAME"), + user=os.getenv("DB_USER"), + password=os.getenv("DB_PASSWORD"), + ) + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE IF NOT EXISTS bank_statement_rows ( + id SERIAL PRIMARY KEY, + tx_date DATE NOT NULL, + description TEXT NOT NULL DEFAULT '', + debit BIGINT NOT NULL DEFAULT 0, + credit BIGINT NOT NULL DEFAULT 0, + balance BIGINT, + memo TEXT NOT NULL DEFAULT '', + bank_name TEXT NOT NULL DEFAULT '', + import_batch_id TEXT NOT NULL, + row_hash TEXT NOT NULL, + imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT uq_bank_stmt_hash UNIQUE (row_hash) + ) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_bank_stmt_date + ON bank_statement_rows (tx_date) + """) + cur.execute(""" + CREATE INDEX IF NOT EXISTS idx_bank_stmt_batch + ON bank_statement_rows (import_batch_id) + """) + conn.commit() + print("✓ bank_statement_rows テーブル確認完了") + conn.close() diff --git a/backend/app/modules/bank_statements/router.py b/backend/app/modules/bank_statements/router.py new file mode 100644 index 0000000..6e56d35 --- /dev/null +++ b/backend/app/modules/bank_statements/router.py @@ -0,0 +1,427 @@ +""" +銀行明細 CSV 取込・照会モジュール +""" +import csv +import hashlib +import io +import uuid +from datetime import datetime, date +from typing import Optional, List + +from fastapi import APIRouter, HTTPException, UploadFile, File, Form +from pydantic import BaseModel + +from app.core.database import get_connection + +router = APIRouter(prefix="/bank-statements", tags=["銀行明細"]) + +# ── CSV 列名検出パターン ────────────────────────────────────── + +_DATE_KEYS = ['日付', '取引日', '年月日', '取引年月日', 'date', '取引日付'] +_DESC_KEYS = ['摘要', '内容', '取引内容', '支払先', '摘要/メモ', '入出金先内容', + 'description', '取引先', 'お取引内容', '取引詳細'] +_DEBIT_KEYS = ['お支払い金額', 'お支払金額', '支払額', '引出し', '出金', '引出額', + '借方', 'debit', '出金金額', '支払い金額', '引き出し金額'] +_CREDIT_KEYS = ['お預かり金額', '預入れ', '預入', '入金', '預入金額', + '貸方', 'credit', '入金金額', 'お預り金額', '預け入れ金額'] +_BALANCE_KEYS = ['残高', '差引残高', 'balance', '帳簿残高', '取引後残高'] +_MEMO_KEYS = ['メモ', '備考', '通帳メモ', 'memo', '注記', '備考欄'] + + +def _find_col(headers: List[str], patterns: List[str]) -> Optional[int]: + lowers = [h.strip().lower() for h in headers] + for pat in patterns: + p = pat.lower() + for i, h in enumerate(lowers): + if p in h: + return i + return None + + +def _parse_amount(s: str) -> int: + if not s: + return 0 + cleaned = (s.strip() + .replace(',', '').replace(',', '') + .replace('¥', '').replace('¥', '') + .replace(' ', '').replace('\u3000', '')) + if cleaned in ('', '-', '―', '-', '▲', 'ー'): + return 0 + try: + return int(float(cleaned)) + except ValueError: + return 0 + + +def _parse_date(s: str) -> Optional[date]: + s = (s.strip() + .replace('/', '-').replace('.', '-') + .replace('年', '-').replace('月', '-').replace('日', '')) + for fmt in ['%Y-%m-%d', '%y-%m-%d']: + try: + return datetime.strptime(s, fmt).date() + except ValueError: + continue + s2 = s.replace('-', '') + if len(s2) == 8: + try: + return datetime.strptime(s2, '%Y%m%d').date() + except ValueError: + pass + if len(s2) == 6: + try: + return datetime.strptime('20' + s2, '%Y%m%d').date() + except ValueError: + pass + return None + + +def _detect_encoding(raw: bytes) -> str: + for enc in ['utf-8-sig', 'utf-8', 'shift_jis', 'cp932', 'euc-jp']: + try: + raw.decode(enc) + return enc + except (UnicodeDecodeError, LookupError): + continue + return 'utf-8' + + +def _make_hash(tx_date: str, description: str, debit: int, + credit: int, balance: Optional[int]) -> str: + s = f"{tx_date}|{description}|{debit}|{credit}|{balance}" + return hashlib.md5(s.encode()).hexdigest() + + +# ── 三菱UFJ MEISAI フォーマット対応 ────────────────────────── +# 行タイプ: "1"=口座ヘッダー, "2"=明細, "8"=フッター, "9"=合計 +# 列: [タイプ, 日付, 摘要区分, 摘要詳細, 出金額, 入金額, 残高] + +def _is_meisai_format(rows: List[List[str]]) -> bool: + """三菱UFJ銀行 MEISAI フォーマット判定""" + type1 = any(r and r[0] == '1' for r in rows) + type2 = any(r and r[0] == '2' for r in rows) + return type1 and type2 + + +def _parse_meisai_rows(rows: List[List[str]]) -> tuple: + """MEISAI フォーマットをパース。(明細リスト, 銀行名ヒント) を返す""" + bank_hint = '' + parsed = [] + + for row in rows: + if not row: + continue + row_type = row[0] + + if row_type == '1' and len(row) >= 3: + # ヘッダー行: [1, 支店コード, 支店名, ..., 科目, 口座番号, 口座名, ...] + branch = row[2].strip() if len(row) > 2 else '' + bank_hint = f"三菱UFJ銀行 {branch}" if branch else '三菱UFJ銀行' + + elif row_type == '2' and len(row) >= 5: + # 明細行: [2, 日付, 摘要区分, 摘要詳細, 出金額, 入金額, 残高] + tx_date = _parse_date(row[1]) + if tx_date is None: + continue + + summary1 = row[2].strip() if len(row) > 2 else '' + summary2 = row[3].strip() if len(row) > 3 else '' + desc = f"{summary1} {summary2}".strip(' ') if summary2 else summary1 + + debit = _parse_amount(row[4]) if len(row) > 4 else 0 + credit = _parse_amount(row[5]) if len(row) > 5 else 0 + bal_str = row[6].strip() if len(row) > 6 else '' + balance = _parse_amount(bal_str) if bal_str else None + + parsed.append({ + 'tx_date': tx_date.isoformat(), + 'description': desc, + 'debit': debit, + 'credit': credit, + 'balance': balance, + 'memo': '', + 'row_hash': _make_hash(tx_date.isoformat(), desc, debit, credit, balance), + }) + + return parsed, bank_hint + + +def _parse_csv_bytes(content: bytes) -> tuple: + """CSVをパース。(明細リスト, 銀行名ヒント) を返す""" + enc = _detect_encoding(content) + text = content.decode(enc).replace('\x00', '').strip() + + reader = csv.reader(io.StringIO(text)) + rows = list(reader) + + if not rows: + raise HTTPException(status_code=400, detail="CSVファイルが空です") + + # MEISAI(三菱UFJ)フォーマット優先検出 + if _is_meisai_format(rows): + return _parse_meisai_rows(rows) + + # ── 汎用ヘッダー行検出 ── + header_row = 0 + for i, row in enumerate(rows[:15]): + row_str = ' '.join(row).lower() + if any(k.lower() in row_str for k in ['日付', '取引日', 'date', '年月日']): + header_row = i + break + + headers = rows[header_row] + date_col = _find_col(headers, _DATE_KEYS) + desc_col = _find_col(headers, _DESC_KEYS) + debit_col = _find_col(headers, _DEBIT_KEYS) + credit_col = _find_col(headers, _CREDIT_KEYS) + balance_col = _find_col(headers, _BALANCE_KEYS) + memo_col = _find_col(headers, _MEMO_KEYS) + + if date_col is None: + raise HTTPException( + status_code=400, + detail="日付列が見つかりません。CSVのヘッダー行に「日付」「取引日」等の列名が必要です。" + ) + + parsed = [] + for row in rows[header_row + 1:]: + if not row or all(c.strip() == '' for c in row): + continue + if len(row) <= date_col: + continue + raw_date = row[date_col].strip() + if not raw_date: + continue + tx_date = _parse_date(raw_date) + if tx_date is None: + continue + + def _safe(col: Optional[int]) -> str: + return row[col].strip() if col is not None and len(row) > col else '' + + desc = _safe(desc_col) + debit = _parse_amount(_safe(debit_col)) + credit = _parse_amount(_safe(credit_col)) + bal_str = _safe(balance_col) + balance = _parse_amount(bal_str) if bal_str else None + memo = _safe(memo_col) + + parsed.append({ + 'tx_date': tx_date.isoformat(), + 'description': desc, + 'debit': debit, + 'credit': credit, + 'balance': balance, + 'memo': memo, + 'row_hash': _make_hash(tx_date.isoformat(), desc, debit, credit, balance), + }) + + return parsed, '' + + +# ── エンドポイント ───────────────────────────────────────────── + +@router.post("/preview", summary="CSVプレビュー(重複チェック)") +async def preview_csv( + file: UploadFile = File(...), + bank_name: str = Form(""), +): + """CSVを解析し、重複チェック結果付きでプレビューを返す(DBには保存しない)""" + content = await file.read() + if len(content) > 10_000_000: + raise HTTPException(status_code=400, detail="ファイルサイズが大きすぎます(上限10MB)") + + rows, bank_hint = _parse_csv_bytes(content) + if not rows: + raise HTTPException(status_code=400, detail="有効なデータ行がありません") + + # 銀行名未入力の場合はCSVから自動検出した値を使用 + if not bank_name.strip() and bank_hint: + bank_name = bank_hint + + # DB上の既存ハッシュを取得して重複チェック + with get_connection() as conn, conn.cursor() as cur: + cur.execute("SELECT row_hash FROM bank_statement_rows") + existing_hashes = {r['row_hash'] for r in cur.fetchall()} + + result = [] + for row in rows: + row['status'] = 'duplicate' if row['row_hash'] in existing_hashes else 'new' + result.append(row) + + new_count = sum(1 for r in result if r['status'] == 'new') + dup_count = sum(1 for r in result if r['status'] == 'duplicate') + + return { + 'total': len(result), + 'new': new_count, + 'duplicates': dup_count, + 'bank_name': bank_name, + 'rows': result, + } + + +class ImportRow(BaseModel): + tx_date: str + description: str + debit: int + credit: int + balance: Optional[int] = None + memo: str = '' + row_hash: str = '' + + +class ImportRequest(BaseModel): + bank_name: str = '' + rows: List[ImportRow] + + +@router.post("/import", summary="CSV取込確定") +def import_rows(req: ImportRequest): + """プレビュー確認後、新規行のみDBに取込む""" + if not req.rows: + raise HTTPException(status_code=400, detail="取込データがありません") + + batch_id = str(uuid.uuid4()) + inserted = 0 + skipped = 0 + + with get_connection() as conn, conn.cursor() as cur: + for row in req.rows: + # row_hash が空の場合は再計算 + rh = row.row_hash or _make_hash( + row.tx_date, row.description, row.debit, row.credit, row.balance + ) + cur.execute(""" + INSERT INTO bank_statement_rows + (tx_date, description, debit, credit, balance, + memo, bank_name, import_batch_id, row_hash) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) + ON CONFLICT (row_hash) DO NOTHING + """, ( + row.tx_date, row.description, + row.debit, row.credit, row.balance, + row.memo, req.bank_name, batch_id, rh, + )) + if cur.rowcount > 0: + inserted += 1 + else: + skipped += 1 + conn.commit() + + return { + 'batch_id': batch_id, + 'inserted': inserted, + 'skipped': skipped, + } + + +@router.get("", summary="明細一覧取得") +def list_statements( + year: Optional[int] = None, + month: Optional[int] = None, + bank_name: Optional[str] = None, + limit: int = 10000, + offset: int = 0, +): + conditions: list = [] + params: list = [] + + if year: + conditions.append("EXTRACT(YEAR FROM tx_date)::int = %s") + params.append(year) + if month: + conditions.append("EXTRACT(MONTH FROM tx_date)::int = %s") + params.append(month) + if bank_name: + conditions.append("bank_name = %s") + params.append(bank_name) + + where_clause = ("WHERE " + " AND ".join(conditions)) if conditions else "" + + with get_connection() as conn, conn.cursor() as cur: + # 明細行 + cur.execute(f""" + SELECT id, + TO_CHAR(tx_date, 'YYYY-MM-DD') AS tx_date, + description, + debit::bigint, + credit::bigint, + balance::bigint, + memo, + bank_name, + import_batch_id + FROM bank_statement_rows + {where_clause} + ORDER BY tx_date, id + LIMIT %s OFFSET %s + """, params + [limit, offset]) + rows = cur.fetchall() + + # 月別集計 + cur.execute(f""" + SELECT + TO_CHAR(tx_date, 'YYYY-MM') AS ym, + SUM(debit)::bigint AS total_debit, + SUM(credit)::bigint AS total_credit, + COUNT(*)::int AS count + FROM bank_statement_rows + {where_clause} + GROUP BY ym + ORDER BY ym + """, params) + monthly = cur.fetchall() + + # 総件数 + cur.execute( + f"SELECT COUNT(*)::int AS cnt FROM bank_statement_rows {where_clause}", + params + ) + total = cur.fetchone()['cnt'] + + # 銀行名リスト + cur.execute(""" + SELECT DISTINCT bank_name + FROM bank_statement_rows + WHERE bank_name != '' + ORDER BY bank_name + """) + banks = [r['bank_name'] for r in cur.fetchall()] + + return { + 'total': total, + 'rows': rows, + 'monthly': monthly, + 'banks': banks, + } + + +@router.get("/batches", summary="取込バッチ一覧") +def list_batches(): + with get_connection() as conn, conn.cursor() as cur: + cur.execute(""" + SELECT + import_batch_id, + bank_name, + MIN(tx_date)::text AS date_from, + MAX(tx_date)::text AS date_to, + COUNT(*)::int AS count, + MIN(imported_at)::text AS imported_at + FROM bank_statement_rows + GROUP BY import_batch_id, bank_name + ORDER BY MIN(imported_at) DESC + LIMIT 100 + """) + return cur.fetchall() + + +@router.delete("/batch/{batch_id}", summary="バッチ削除") +def delete_batch(batch_id: str): + with get_connection() as conn, conn.cursor() as cur: + cur.execute( + "DELETE FROM bank_statement_rows WHERE import_batch_id = %s", + (batch_id,) + ) + deleted = cur.rowcount + conn.commit() + return {'deleted': deleted} diff --git a/create_bank_table.py b/create_bank_table.py new file mode 100644 index 0000000..c4c7556 --- /dev/null +++ b/create_bank_table.py @@ -0,0 +1,63 @@ +"""銀行明細テーブルを直接作成する""" +import paramiko + +HOST = "192.168.0.61" +USER = "root" +PASS = "59911784" + +SQL = """ +CREATE TABLE IF NOT EXISTS bank_statement_rows ( + id SERIAL PRIMARY KEY, + tx_date DATE NOT NULL, + description TEXT NOT NULL DEFAULT '', + debit BIGINT NOT NULL DEFAULT 0, + credit BIGINT NOT NULL DEFAULT 0, + balance BIGINT, + memo TEXT NOT NULL DEFAULT '', + bank_name TEXT NOT NULL DEFAULT '', + import_batch_id TEXT NOT NULL, + row_hash TEXT NOT NULL, + imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT uq_bank_stmt_hash UNIQUE (row_hash) +); +CREATE INDEX IF NOT EXISTS idx_bank_stmt_date ON bank_statement_rows (tx_date); +CREATE INDEX IF NOT EXISTS idx_bank_stmt_batch ON bank_statement_rows (import_batch_id); +""" + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect(HOST, username=USER, password=PASS, timeout=15) + +cmd = f"""docker exec njts-accounting-backend python -c " +import os, psycopg +conn = psycopg.connect(host=os.getenv('DB_HOST'), port=os.getenv('DB_PORT'), dbname=os.getenv('DB_NAME'), user=os.getenv('DB_USER'), password=os.getenv('DB_PASSWORD')) +cur = conn.cursor() +cur.execute(\\\"\\\"\\\" +CREATE TABLE IF NOT EXISTS bank_statement_rows ( + id SERIAL PRIMARY KEY, + tx_date DATE NOT NULL, + description TEXT NOT NULL DEFAULT '', + debit BIGINT NOT NULL DEFAULT 0, + credit BIGINT NOT NULL DEFAULT 0, + balance BIGINT, + memo TEXT NOT NULL DEFAULT '', + bank_name TEXT NOT NULL DEFAULT '', + import_batch_id TEXT NOT NULL, + row_hash TEXT NOT NULL, + imported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT uq_bank_stmt_hash UNIQUE (row_hash) +) +\\\"\\\"\\\") +cur.execute('CREATE INDEX IF NOT EXISTS idx_bank_stmt_date ON bank_statement_rows (tx_date)') +cur.execute('CREATE INDEX IF NOT EXISTS idx_bank_stmt_batch ON bank_statement_rows (import_batch_id)') +conn.commit() +print('テーブル作成完了') +conn.close() +" +""" + +_, o, e = client.exec_command(cmd, timeout=30) +print(o.read().decode()) +err = e.read().decode() +if err: print("STDERR:", err) +client.close() diff --git a/docs/MEISAI20260531234440.csv b/docs/MEISAI20260531234440.csv new file mode 100644 index 0000000..dd87387 --- /dev/null +++ b/docs/MEISAI20260531234440.csv @@ -0,0 +1,32 @@ +"1","082","юxX","1","0","","0788058","V{eNm\[VY@","2026.4.1-2026.5.31","S","2026.5.31","23:44","","","" +"2","2026.4.15","UP","Jjj|\|VY","0","630000","17158067" +"2","2026.4.20","UւR","`E^CLEJPL","41000","0","17117067" +"2","2026.4.28","UւR","yg[CVzEVE","11000","0","17106067" +"2","2026.4.30","Љی","","247066","0","16859001" +"2","2026.4.30","UP","XYV|giJ","0","1606000","18465001" +"2","2026.4.30","UayQ","JjGtP|GX","170000","0","18295001" +"2","2026.4.30","ב֎萔","tR~@eXEE","660","0","18294341" +"2","2026.4.30","UayP","IE@JN","353499","0","17940842" +"2","2026.4.30","ב֎萔","tR~@eXEE","330","0","17940512" +"2","2026.4.30","UayP","`E@VEJN","4026825","0","13913687" +"2","2026.4.30","ב֎萔","tR~@eXEE","330","0","13913357" +"2","2026.5.1","J|h","","1000000","0","12913357" +"2","2026.5.7","J|h","","1000000","0","11913357" +"2","2026.5.15","UP","Jjj|\|VY","0","630000","12543357" +"2","2026.5.18","UւR","`E^CLEJPL","41000","0","12502357" +"2","2026.5.18","UP","G^c@iJ","0","4620000","17122357" +"2","2026.5.20","UayP","IE@JN","2787776","0","14334581" +"2","2026.5.20","ב֎萔","tR~@eXEE","330","0","14334251" +"2","2026.5.28","UւR","yg[CVzEVE","11000","0","14323251" +"2","2026.5.29","UayQ","JjGtP|GX","170000","0","14153251" +"2","2026.5.29","ב֎萔","tR~@eXEE","660","0","14152591" +"2","2026.5.29","UayQ","`EVEJN","350781","0","13801810" +"2","2026.5.29","ב֎萔","tR~@eXEE","660","0","13801150" +"2","2026.5.29","UayP","IE@JN","353091","0","13448059" +"2","2026.5.29","ב֎萔","tR~@eXEE","330","0","13447729" +"2","2026.5.29","UayQ","G^c@iJ","824340","0","12623389" +"2","2026.5.29","ב֎萔","tR~@eXEE","660","0","12622729" +"2","2026.5.29","UP","XYV|giJ","0","1694000","14316729" +"2","2026.5.29","UP","G^c@iJ","0","825000","15141729" +"8" +"9","23","6","","11391338","10005000","16528067","15141729" diff --git a/docs/三菱UFJ_APIのアカウント資料.xlsx b/docs/三菱UFJ_APIのアカウント資料.xlsx new file mode 100644 index 0000000..2095f3f Binary files /dev/null and b/docs/三菱UFJ_APIのアカウント資料.xlsx differ diff --git a/frontend/bank-statement.html b/frontend/bank-statement.html new file mode 100644 index 0000000..470ca86 --- /dev/null +++ b/frontend/bank-statement.html @@ -0,0 +1,768 @@ + + + + + + + 銀行明細照会 + + + + + + + +
+ + +
+ + + + + + + +
+ + +
+ + +
+ + +
+
データを読み込み中…
+
+ + + + + + + + diff --git a/frontend/chat/Caddyfile b/frontend/chat/Caddyfile new file mode 100644 index 0000000..5bc2ce7 --- /dev/null +++ b/frontend/chat/Caddyfile @@ -0,0 +1,62 @@ +chat.njts.co.jp:2015 { + tls /certs/chat.njts.co.jp/fullchain.pem /certs/chat.njts.co.jp/key.pem + reverse_proxy rocketchat:3000 +} + +nas.njts.co.jp:2015 { + tls /certs/nas.njts.co.jp/fullchain.pem /certs/nas.njts.co.jp/key.pem + reverse_proxy 192.168.0.61:5666 +} + +synas.njts.co.jp:2015 { + tls /certs/synas.njts.co.jp/fullchain.pem /certs/synas.njts.co.jp/key.pem + reverse_proxy 192.168.0.193:5000 +} + +meet.njts.co.jp:2015 { + tls /certs/meet.njts.co.jp/fullchain.pem /certs/meet.njts.co.jp/key.pem + + reverse_proxy jitsi-web-1:80 +} + + +matrix.njts.co.jp:2015 { + tls /certs/matrix.njts.co.jp/fullchain.pem /certs/matrix.njts.co.jp/key.pem + + reverse_proxy matrix-synapse:8008 +} + +element.njts.co.jp:2015 { + tls /certs/element.njts.co.jp/fullchain.pem /certs/element.njts.co.jp/key.pem + reverse_proxy element-web:80 +} + +router.njts.co.jp:2015 { + tls /certs/router.njts.co.jp/fullchain.pem /certs/router.njts.co.jp/key.pem + reverse_proxy 192.168.0.1:80 +} + +cmr.njts.co.jp:2015 { + + basicauth { + zhangxianghe $2a$14$a00NihP7NRLzd3j5EvFa2uFYXHJ.NoWyCcgKA3NKpUG22607HN/Ri + } + + tls /certs/cmr.njts.co.jp/fullchain.pem /certs/cmr.njts.co.jp/key.pem + reverse_proxy 192.168.0.61:5000 +} + +openclaw.njts.co.jp:2015 { + tls /certs/openclaw.njts.co.jp/fullchain.pem /certs/openclaw.njts.co.jp/key.pem + reverse_proxy 192.168.0.61:18789 +} + +chatai.njts.co.jp:2015 { + tls /certs/chatai.njts.co.jp/fullchain.pem /certs/chatai.njts.co.jp/key.pem + reverse_proxy 192.168.0.61:3000 +} + +sys.njts.co.jp:2015 { + tls /certs/sys.njts.co.jp/fullchain.pem /certs/sys.njts.co.jp/key.pem + reverse_proxy 192.168.0.61:18000 +} diff --git a/frontend/chat/caddy_config/caddy/autosave.json b/frontend/chat/caddy_config/caddy/autosave.json new file mode 100644 index 0000000..a021c5a --- /dev/null +++ b/frontend/chat/caddy_config/caddy/autosave.json @@ -0,0 +1 @@ +{"apps":{"http":{"servers":{"srv0":{"listen":[":8443"],"routes":[{"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"rocketchat:3000"}]}]}]}],"match":[{"host":["chat.njts.co.jp"]}],"terminal":true}]}}}}} \ No newline at end of file diff --git a/frontend/chat/caddy_data/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default/default.json b/frontend/chat/caddy_data/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default/default.json new file mode 100644 index 0000000..3156648 --- /dev/null +++ b/frontend/chat/caddy_data/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default/default.json @@ -0,0 +1,5 @@ +{ + "status": "valid", + "termsOfServiceAgreed": true, + "location": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/254799123" +} \ No newline at end of file diff --git a/frontend/chat/caddy_data/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default/default.key b/frontend/chat/caddy_data/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default/default.key new file mode 100644 index 0000000..17d4729 --- /dev/null +++ b/frontend/chat/caddy_data/caddy/acme/acme-staging-v02.api.letsencrypt.org-directory/users/default/default.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKj+HhAaYqX1lWm4mLP2Et8b5rxVmCuGgMZpfvT/KEN4oAoGCCqGSM49 +AwEHoUQDQgAED9JJ499KtFgQS78wE5cxUs3iQoBq75s3my0knVRJah0H3VwwOtJq +vKMmgyX4vUT0fDTYfgrvrLALcJ9QT4Bx9Q== +-----END EC PRIVATE KEY----- diff --git a/frontend/chat/caddy_data/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default/default.json b/frontend/chat/caddy_data/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default/default.json new file mode 100644 index 0000000..3685ac8 --- /dev/null +++ b/frontend/chat/caddy_data/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default/default.json @@ -0,0 +1,5 @@ +{ + "status": "valid", + "termsOfServiceAgreed": true, + "location": "https://acme-v02.api.letsencrypt.org/acme/acct/2926552736" +} \ No newline at end of file diff --git a/frontend/chat/caddy_data/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default/default.key b/frontend/chat/caddy_data/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default/default.key new file mode 100644 index 0000000..ecb8a3b --- /dev/null +++ b/frontend/chat/caddy_data/caddy/acme/acme-v02.api.letsencrypt.org-directory/users/default/default.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIDs4tIRZUhyrIoIgZSO6ZBAYg+HzW0l0j2OlVoMzJY6ioAoGCCqGSM49 +AwEHoUQDQgAEv375la4g5tzk4LddVNaLZ7jBelpGzkADYERdZzELLLFL0NEciAa1 +Lus7P6OxGoc30AsCw34co3SXMhWEp3gsAg== +-----END EC PRIVATE KEY----- diff --git a/frontend/chat/caddy_data/caddy/instance.uuid b/frontend/chat/caddy_data/caddy/instance.uuid new file mode 100644 index 0000000..5f290c4 --- /dev/null +++ b/frontend/chat/caddy_data/caddy/instance.uuid @@ -0,0 +1 @@ +268cd375-73e5-4ccb-88ca-c0051a74b1f2 \ No newline at end of file diff --git a/frontend/chat/caddy_data/caddy/last_clean.json b/frontend/chat/caddy_data/caddy/last_clean.json new file mode 100644 index 0000000..d081d7c --- /dev/null +++ b/frontend/chat/caddy_data/caddy/last_clean.json @@ -0,0 +1 @@ +{"tls":{"timestamp":"2026-01-01T11:51:10.149840886Z","instance_id":"268cd375-73e5-4ccb-88ca-c0051a74b1f2"}} \ No newline at end of file diff --git a/frontend/chat/certs/fullchain.pem b/frontend/chat/certs/fullchain.pem new file mode 100644 index 0000000..0aae82e --- /dev/null +++ b/frontend/chat/certs/fullchain.pem @@ -0,0 +1,68 @@ +-----BEGIN CERTIFICATE----- +MIID9jCCA3ygAwIBAgIQcfzZIhLrRJHPhJLzeOAN5DAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJBVDEQMA4GA1UEChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBF +Q0MgRG9tYWluIFNlY3VyZSBTaXRlIENBMB4XDTI1MTIzMTAwMDAwMFoXDTI2MDMz +MTIzNTk1OVowGjEYMBYGA1UEAxMPY2hhdC5uanRzLmNvLmpwMFkwEwYHKoZIzj0C +AQYIKoZIzj0DAQcDQgAEOp7tnlqOPWi6GswskLGaQ7J8pjU5YQjlNgPXVYr93VWT +Wp5oBt+Q1EPZoLSErJ05xzVgYTzzPf5Z92jk+crZsqOCAnEwggJtMB8GA1UdIwQY +MBaAFA9r5kvOOUeu9n6QHnnwMJGSyF+jMB0GA1UdDgQWBBQsF22yoIoKX6aCGL6f +2veSYHlq1TAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAK +BggrBgEFBQcDATBJBgNVHSAEQjBAMDQGCysGAQQBsjEBAgJOMCUwIwYIKwYBBQUH +AgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBiAYIKwYBBQUH +AQEEfDB6MEsGCCsGAQUFBzAChj9odHRwOi8vemVyb3NzbC5jcnQuc2VjdGlnby5j +b20vWmVyb1NTTEVDQ0RvbWFpblNlY3VyZVNpdGVDQS5jcnQwKwYIKwYBBQUHMAGG +H2h0dHA6Ly96ZXJvc3NsLm9jc3Auc2VjdGlnby5jb20wggEEBgorBgEEAdZ5AgQC +BIH1BIHyAPAAdgAOV5S8866pPjMbLJkHs/eQ35vCPXEyJd0hqSWsYcVOIQAAAZtz +uvUJAAAEAwBHMEUCIHjB8YtQ9aDlxSRut60eoDQ/h7fraLoPxLOEsBB/Hu/AAiEA +5USOxiL99VrBdF4HTcJoM2AomuA+MvmpfVcsr1bXUqQAdgDRbqmlaAd+ZjWgPzel +3bwDpTxBEhTUiBj16TGzI8uVBAAAAZtzuvXXAAAEAwBHMEUCIBGuQL/+LnZdckCW +jGHUMPwztijW/8gfzcK8j3ULmm6FAiEAzI2YGnTwt5rYKIVaVU9nZGuATFlx9gtB +iiz2Roppz9UwGgYDVR0RBBMwEYIPY2hhdC5uanRzLmNvLmpwMAoGCCqGSM49BAMD +A2gAMGUCMQDb3BnY/YT49vWBxF1/h4eX26Ayn+5AEzn7ZcDpSVl20pAwJsqRjn7f +hodKgvtO3Y0CMDAkUJFzHKY4VP2oVhtldYIgfveAauS3YYGFEY25NVnVI224NSt6 +CiUqiJf/0Pjpdw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDhTCCAwygAwIBAgIQI7dt48G7KxpRlh4I6rdk6DAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAwMTMw +MDAwMDAwWhcNMzAwMTI5MjM1OTU5WjBLMQswCQYDVQQGEwJBVDEQMA4GA1UEChMH +WmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBFQ0MgRG9tYWluIFNlY3VyZSBTaXRl +IENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAENkFhFytTJe2qypTk1tpIV+9QuoRk +gte7BRvWHwYk9qUznYzn8QtVaGOCMBBfjWXsqqivl8q1hs4wAYl03uNOXgFu7iZ7 +zFP6I6T3RB0+TR5fZqathfby47yOCZiAJI4go4IBdTCCAXEwHwYDVR0jBBgwFoAU +OuEJhtTPGcKWdnRJdtzgNcZjY5owHQYDVR0OBBYEFA9r5kvOOUeu9n6QHnnwMJGS +yF+jMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdJQQW +MBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEBAgJO +MAgGBmeBDAECATBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVz +dC5jb20vVVNFUlRydXN0RUNDQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwdgYI +KwYBBQUHAQEEajBoMD8GCCsGAQUFBzAChjNodHRwOi8vY3J0LnVzZXJ0cnVzdC5j +b20vVVNFUlRydXN0RUNDQWRkVHJ1c3RDQS5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6 +Ly9vY3NwLnVzZXJ0cnVzdC5jb20wCgYIKoZIzj0EAwMDZwAwZAIwJHBUDwHJQN3I +VNltVMrICMqYQ3TYP/TXqV9t8mG5cAomG2MwqIsxnL937Gewf6WIAjAlrauksO6N +UuDdDXyd330druJcZJx0+H5j5cFOYBaGsKdeGW7sCMaR2PsDFKGllas= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID0zCCArugAwIBAgIQVmcdBOpPmUxvEIFHWdJ1lDANBgkqhkiG9w0BAQwFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 +MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO +ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgRUNDIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEGqxUWqn5aCPnetUkb1PGWthL +q8bVttHmc3Gu3ZzWDGH926CJA7gFFOxXzu5dP+Ihs8731Ip54KODfi2X0GHE8Znc +JZFjq38wo7Rw4sehM5zzvy5cU7Ffs30yf4o043l5o4HyMIHvMB8GA1UdIwQYMBaA +FKARCiM+lvEH7OKvKe+CpX/QMKS0MB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1 +xmNjmjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAI +MAYGBFUdIAAwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5j +b20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEEKDAmMCQG +CCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZIhvcNAQEM +BQADggEBABns652JLCALBIAdGN5CmXKZFjK9Dpx1WywV4ilAbe7/ctvbq5AfjJXy +ij0IckKJUAfiORVsAYfZFhr1wHUrxeZWEQff2Ji8fJ8ZOd+LygBkc7xGEJuTI42+ +FsMuCIKchjN0djsoTI0DQoWz4rIjQtUfenVqGtF8qmchxDM6OW1TyaLtYiKou+JV +bJlsQ2uRl9EMC5MCHdK8aXdJ5htN978UeAOwproLtOGFfy/cQjutdAFI3tZs4RmY +CV4Ks2dH/hzg1cEo70qLRDEmBDeNiXQ2Lu+lIg+DdEmSx/cQwgwp+7e9un/jX9Wf +8qn0dNW44bOwgeThpWOjzOoEeJBuv/c= +-----END CERTIFICATE----- diff --git a/frontend/chat/certs/key.pem b/frontend/chat/certs/key.pem new file mode 100644 index 0000000..6a351d7 --- /dev/null +++ b/frontend/chat/certs/key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIPjpQxKBjFFc8UvRnWDDKX2OP+ypSHXGYZ2cpRgPBP3koAoGCCqGSM49 +AwEHoUQDQgAEOp7tnlqOPWi6GswskLGaQ7J8pjU5YQjlNgPXVYr93VWTWp5oBt+Q +1EPZoLSErJ05xzVgYTzzPf5Z92jk+crZsg== +-----END EC PRIVATE KEY----- diff --git a/frontend/chat/docker-compose.yml b/frontend/chat/docker-compose.yml new file mode 100644 index 0000000..1e11936 --- /dev/null +++ b/frontend/chat/docker-compose.yml @@ -0,0 +1,62 @@ +version: "3.8" + +services: + rocketchat: + image: registry.rocket.chat/rocketchat/rocket.chat:8.2.1 + container_name: rocketchat + restart: unless-stopped + environment: + MONGO_URL: mongodb://rocketchat-mongo:27017/rocketchat?replicaSet=rs0 + MONGO_OPLOG_URL: mongodb://rocketchat-mongo:27017/local?replicaSet=rs0 + ROOT_URL: https://chat.njts.co.jp + PORT: 3000 + depends_on: + - mongo + networks: + - chat + + mongo: + image: mongo:8.0.13 + container_name: rocketchat-mongo + restart: unless-stopped + command: mongod --replSet rs0 --oplogSize 128 + volumes: + - mongo-data:/data/db + networks: + - chat + + mongo-init: + image: mongo:8.0.13 + restart: "no" + depends_on: + - mongo + command: > + mongosh --host mongo:27017 --eval + "rs.initiate({_id:'rs0',members:[{_id:0,host:'mongo:27017'}]})" + networks: + - chat + + caddy: + image: caddy:2 + container_name: caddy + restart: unless-stopped + ports: + - "2015:2015" + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - /etc/caddy/certs:/certs:ro + - caddy_data:/data + - caddy_config:/config + networks: + - chat + - shared_net + +volumes: + mongo-data: + caddy_data: + caddy_config: + +networks: + chat: + shared_net: + external: true \ No newline at end of file diff --git a/frontend/css/mobile.css b/frontend/css/mobile.css index 3c2deb7..3db920f 100644 --- a/frontend/css/mobile.css +++ b/frontend/css/mobile.css @@ -323,4 +323,58 @@ html { width: 100% !important; box-sizing: border-box !important; } + + /* ── split-layout: PC は左右分割、モバイルは上下積み ── */ + /* 左リスト・右明細を縦方向に並べ直す */ + .split-layout { + flex-direction: column !important; + } + + .split-left { + flex: none !important; + width: 100% !important; + max-width: none !important; + min-width: 0 !important; + max-height: none !important; + overflow-y: visible !important; + position: static !important; + } + + .split-right { + width: 100% !important; + min-width: 0 !important; + border-left: none !important; + border-top: 2px solid #e0e8f0 !important; + padding-left: 0 !important; + padding-top: 16px !important; + margin-top: 8px !important; + max-height: none !important; + overflow-y: visible !important; + position: static !important; + } + + /* ── タブ(給与計算・賞与計算・帳票出力)を折り返し ── */ + .nav-tabs-calc { + flex-wrap: wrap !important; + gap: 6px !important; + border-bottom: none !important; + } + + .nav-tab-calc { + padding: 8px 14px !important; + font-size: 0.9rem !important; + border-radius: 6px !important; + } + + /* ── 新規計算モーダル(inline style の width:800px を上書き) ── */ + #calculateModal, + #bonusModal { + width: 92vw !important; + left: 4vw !important; + transform: none !important; + max-width: 92vw !important; + padding: 20px 16px !important; + box-sizing: border-box !important; + } + } diff --git a/frontend/egov.html b/frontend/egov.html index be51c33..5878da5 100644 --- a/frontend/egov.html +++ b/frontend/egov.html @@ -284,7 +284,7 @@

🏛 e-Gov 電子送達

- ← メニューに戻る + ← メニューへ戻る
diff --git a/frontend/index.html b/frontend/index.html index 4928441..441d1a7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,8 @@ + + NJTS 会計システム - メイン @@ -190,6 +192,16 @@

給与システムを開く

+
+

🏦 銀行明細照会

+

+ 銀行のCSVデータを取込み、明細を管理します。重複チェック付きで安全に取込め、月別・銀行別の検索・印刷が可能です。 +

+

+ 銀行明細を開く +

+
+

🏛 e-Gov 電子送達

@@ -263,18 +275,6 @@ } } - // dropdown toggle - const toggle = document.getElementById("accountingToggle"); - const menu = document.getElementById("accountingMenu"); - toggle.addEventListener("click", (e) => { - menu.style.display = menu.style.display === "block" ? "none" : "block"; - }); - // click outside to close - document.addEventListener("click", (e) => { - if (!toggle.contains(e.target) && !menu.contains(e.target)) { - menu.style.display = "none"; - } - }); diff --git a/frontend/journal-entry.html b/frontend/journal-entry.html index 7b3921a..8d2f533 100644 --- a/frontend/journal-entry.html +++ b/frontend/journal-entry.html @@ -257,22 +257,7 @@ - + ← メニューへ戻る

仕訳入力

diff --git a/frontend/opening_balance.html b/frontend/opening_balance.html index b8d0b93..dd1753d 100644 --- a/frontend/opening_balance.html +++ b/frontend/opening_balance.html @@ -56,21 +56,7 @@ > 保存 - + ← メニューへ戻る diff --git a/frontend/payroll-calculation.html b/frontend/payroll-calculation.html index 6f029ad..6bee2b4 100644 --- a/frontend/payroll-calculation.html +++ b/frontend/payroll-calculation.html @@ -1237,6 +1237,7 @@ ? ((_child / _stdRemun) * 100).toFixed(3) : null; const _socialSub = _health + _care + _pension + _emp + _child; + const _healthCareChild = _health + _care + _child; const _taxable = _totalPay - _socialSub; const html = ` @@ -1328,6 +1329,10 @@ : "令和8年5月支払分(4月分保険料)より控除開始 → 当月は対象外" }
+
+ 健康保険 ¥${_health.toLocaleString()} + 介護保険 ¥${_care.toLocaleString()} + 子ども・子育て支援金 ¥${_child.toLocaleString()} = ¥${_healthCareChild.toLocaleString()}
+ ¥${_healthCareChild.toLocaleString()}(上記合計) + 厚生年金 ¥${_pension.toLocaleString()}${' '}${_emp > 0 ? `+ 雇用保険 ¥${_emp.toLocaleString()}` : ''}= 社会保険小計 ¥${_socialSub.toLocaleString()} +
社会保険小計:¥${_socialSub.toLocaleString()}
所得税 (甲欄${dependentCount}人): @@ -1372,6 +1377,10 @@ const splitRightSalary = document.getElementById("splitRightSalary"); if (splitRightSalary) splitRightSalary.style.display = "block"; document.getElementById("payrollDetail").style.display = "block"; + // モバイルでは明細パネルへ自動スクロール + if (window.innerWidth <= 767 && splitRightSalary) { + setTimeout(function() { splitRightSalary.scrollIntoView({ behavior: "smooth", block: "start" }); }, 50); + } } catch (error) { alert("給与明細の取得に失敗しました"); console.error(error); @@ -1387,7 +1396,7 @@ // 編集フォームを表示 const html = ` -
+

給与データ編集

@@ -1842,6 +1851,7 @@ ? ((_bChild / _bTotal) * 100).toFixed(4) : null; const _bSocialSub = _bHealth + _bCare + _bPension + _bEmp + _bChild; + const _bHealthCareChild = _bHealth + _bCare + _bChild; const _bTaxable = _bTotal - _bSocialSub; const html = ` @@ -1906,6 +1916,10 @@ : "令和8年5月支払分(4月分保険料)より控除開始 → 当賞与は対象外" }
+
+ 健康保険 ¥${_bHealth.toLocaleString()} + 介護保険 ¥${_bCare.toLocaleString()} + 子ども・子育て支援金 ¥${_bChild.toLocaleString()} = ¥${_bHealthCareChild.toLocaleString()}
+ ¥${_bHealthCareChild.toLocaleString()}(上記合計) + 厚生年金 ¥${_bPension.toLocaleString()}${' '}${_bEmp > 0 ? `+ 雇用保険 ¥${_bEmp.toLocaleString()}` : ''}= 社会保険小計 ¥${_bSocialSub.toLocaleString()} +
社会保険小計:¥${_bSocialSub.toLocaleString()}
所得税 (甲欄${bDependentCount}人): @@ -1948,6 +1962,10 @@ const splitRightBonus = document.getElementById("splitRightBonus"); if (splitRightBonus) splitRightBonus.style.display = "block"; document.getElementById("bonusDetail").style.display = "block"; + // モバイルでは明細パネルへ自動スクロール + if (window.innerWidth <= 767 && splitRightBonus) { + setTimeout(function() { splitRightBonus.scrollIntoView({ behavior: "smooth", block: "start" }); }, 50); + } } catch (error) { alert("賞与明細の取得に失敗しました"); console.error(error); @@ -1960,7 +1978,7 @@ const bonus = await response.json(); const html = ` -
+

賞与データ編集

diff --git a/frontend/payroll.html b/frontend/payroll.html index e32f8ef..f6c4d0a 100644 --- a/frontend/payroll.html +++ b/frontend/payroll.html @@ -56,21 +56,7 @@
- + ← メニューへ戻る

給与管理システム

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

diff --git a/frontend/trial-balance.html b/frontend/trial-balance.html index 4788b6b..f18b6b0 100644 --- a/frontend/trial-balance.html +++ b/frontend/trial-balance.html @@ -267,22 +267,7 @@ - + ← メニューへ戻る

残高試算表(貸借・損益)

diff --git a/nas_deploy.py b/nas_deploy.py new file mode 100644 index 0000000..327c955 --- /dev/null +++ b/nas_deploy.py @@ -0,0 +1,68 @@ +"""NASにファイルを転送してコンテナを再起動するスクリプト""" +import paramiko +import os + +HOST = "192.168.0.61" +USER = "root" +PASS = "59911784" +NAS_BASE = "/volume1/docker/njts-accounting" + +files = [ + # (ローカルパス, NASパス) + (r"c:\workspace\njts-accounting-core\backend\app\modules\bank_statements\__init__.py", + f"{NAS_BASE}/backend/app/modules/bank_statements/__init__.py"), + (r"c:\workspace\njts-accounting-core\backend\app\modules\bank_statements\router.py", + f"{NAS_BASE}/backend/app/modules/bank_statements/router.py"), + (r"c:\workspace\njts-accounting-core\backend\app\modules\bank_statements\migration.py", + f"{NAS_BASE}/backend/app/modules/bank_statements/migration.py"), + (r"c:\workspace\njts-accounting-core\backend\app\db_auto_migration.py", + f"{NAS_BASE}/backend/app/db_auto_migration.py"), + (r"c:\workspace\njts-accounting-core\backend\app\main.py", + f"{NAS_BASE}/backend/app/main.py"), + (r"c:\workspace\njts-accounting-core\frontend\bank-statement.html", + f"{NAS_BASE}/backend/frontend/bank-statement.html"), + (r"c:\workspace\njts-accounting-core\frontend\index.html", + f"{NAS_BASE}/backend/frontend/index.html"), + (r"c:\workspace\njts-accounting-core\frontend\journal-entry.html", + f"{NAS_BASE}/backend/frontend/journal-entry.html"), + (r"c:\workspace\njts-accounting-core\frontend\trial-balance.html", + f"{NAS_BASE}/backend/frontend/trial-balance.html"), + (r"c:\workspace\njts-accounting-core\frontend\opening_balance.html", + f"{NAS_BASE}/backend/frontend/opening_balance.html"), + (r"c:\workspace\njts-accounting-core\frontend\egov.html", + f"{NAS_BASE}/backend/frontend/egov.html"), + (r"c:\workspace\njts-accounting-core\frontend\payroll.html", + f"{NAS_BASE}/backend/frontend/payroll.html"), +] + +client = paramiko.SSHClient() +client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) +client.connect(HOST, username=USER, password=PASS, timeout=30) +print(f"SSH接続: {HOST}") + +# ディレクトリ作成 +stdin, stdout, stderr = client.exec_command( + f"mkdir -p {NAS_BASE}/backend/app/modules/bank_statements && mkdir -p {NAS_BASE}/backend/frontend" +) +stdout.read() +print("ディレクトリ作成完了") + +# SFTPでファイル転送 +sftp = client.open_sftp() +for local_path, remote_path in files: + sftp.put(local_path, remote_path) + print(f"転送: {os.path.basename(local_path)} → {remote_path}") +sftp.close() + +# コンテナ再起動 +print("\nコンテナ再起動中...") +cmd = f"cd {NAS_BASE} && docker compose restart backend" +stdin, stdout, stderr = client.exec_command(cmd, timeout=60) +out = stdout.read().decode() +err = stderr.read().decode() +code = stdout.channel.recv_exit_status() +if out: print("stdout:", out) +if err: print("stderr:", err) +print(f"再起動完了 (exit={code})") +client.close() +print("完了!")