from flask import Flask, render_template, request, jsonify, redirect, url_for, send_file, send_from_directory, session, flash
from flask_sqlalchemy import SQLAlchemy
from werkzeug.security import generate_password_hash, check_password_hash
from functools import wraps
from datetime import datetime, date, timedelta
from collections import defaultdict
import json
import io
import os
import shutil
import zipfile
import threading
import urllib.request

app = Flask(__name__)

# Gunakan absolute path untuk database SQLite agar stabil di Passenger/cPanel
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
INSTANCE_DIR = os.path.join(BASE_DIR, 'instance')
os.makedirs(INSTANCE_DIR, exist_ok=True)
DB_PATH = os.path.join(INSTANCE_DIR, 'pengajuan.db')

app.config['SQLALCHEMY_DATABASE_URI'] = f"sqlite:///{DB_PATH}"
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'sppg-sendangrejo-2024'

db = SQLAlchemy(app)

# ─────────────────────────────────────────
# MODELS
# ─────────────────────────────────────────

class User(db.Model):
    """Model Pengguna / Akun untuk Autentikasi dan Role-Based Access Control."""
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    nama_lengkap = db.Column(db.String(120), nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    role = db.Column(db.String(30), default='admin_dapur')  # 'super_admin', 'validator', 'admin_dapur', 'viewer'
    dapur_id = db.Column(db.Integer, db.ForeignKey('dapur.id'), nullable=True)  # terikat ke dapur tertentu jika role admin_dapur
    aktif = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    dapur = db.relationship('Dapur', backref='users')

    def set_password(self, password):
        self.password_hash = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password_hash, password)

    def to_dict(self):
        return {
            'id': self.id,
            'username': self.username,
            'nama_lengkap': self.nama_lengkap,
            'role': self.role,
            'dapur_id': self.dapur_id,
            'dapur_nama': self.dapur.nama if self.dapur else None,
            'aktif': self.aktif,
            'created_at': self.created_at.strftime('%Y-%m-%d %H:%M') if self.created_at else ''
        }


class Pemasok(db.Model):
    __tablename__ = 'pemasok'
    id = db.Column(db.Integer, primary_key=True)
    nama = db.Column(db.String(150), nullable=False)
    bank = db.Column(db.String(50), nullable=False)
    no_rekening = db.Column(db.String(50), nullable=False)
    kategori = db.Column(db.String(50), nullable=True)  # opsional filter kategori
    tipe = db.Column(db.String(30), default='BAHAN')    # 'BAHAN', 'OPERASIONAL', 'SEMUA'
    aktif = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def to_dict(self):
        return {
            'id': self.id,
            'nama': self.nama,
            'bank': self.bank,
            'no_rekening': self.no_rekening,
            'kategori': self.kategori,
            'tipe': self.tipe or 'BAHAN',
            'aktif': self.aktif,
            'label': f"{self.nama} / {self.bank} / {self.no_rekening}"
        }


class PengajuanHeader(db.Model):
    __tablename__ = 'pengajuan_header'
    id = db.Column(db.Integer, primary_key=True)
    nomor = db.Column(db.String(30), unique=True, nullable=False)
    lokasi = db.Column(db.String(100), default='SPPG SENDANGREJO')
    tanggal = db.Column(db.Date, nullable=False)
    kategori = db.Column(db.String(50), nullable=False)
    pemasok_id = db.Column(db.Integer, db.ForeignKey('pemasok.id'), nullable=True)
    total = db.Column(db.Float, default=0)
    status = db.Column(db.String(20), default='draft')  # draft, submitted, approved
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    pemasok = db.relationship('Pemasok', backref='pengajuan_list')
    items = db.relationship('PengajuanItem', backref='header', cascade='all, delete-orphan')

    def to_dict(self):
        return {
            'id': self.id,
            'nomor': self.nomor,
            'lokasi': self.lokasi,
            'tanggal': self.tanggal.strftime('%Y-%m-%d') if self.tanggal else '',
            'kategori': self.kategori,
            'pemasok_id': self.pemasok_id,
            'pemasok': self.pemasok.to_dict() if self.pemasok else None,
            'total': self.total,
            'status': self.status,
            'items': [i.to_dict() for i in self.items],
        }


class PengajuanItem(db.Model):
    __tablename__ = 'pengajuan_item'
    id = db.Column(db.Integer, primary_key=True)
    header_id = db.Column(db.Integer, db.ForeignKey('pengajuan_header.id'), nullable=False)
    nama_barang = db.Column(db.String(150), nullable=False)
    qty = db.Column(db.Float, default=0)
    satuan = db.Column(db.String(30), default='')
    harga_satuan = db.Column(db.Float, default=0)
    subtotal = db.Column(db.Float, default=0)

    def to_dict(self):
        return {
            'id': self.id,
            'nama_barang': self.nama_barang,
            'qty': self.qty,
            'satuan': self.satuan,
            'harga_satuan': self.harga_satuan,
            'subtotal': self.subtotal,
        }


class Dapur(db.Model):
    """Daftar dapur / lokasi yang bisa dipilih saat input validasi belanja."""
    __tablename__ = 'dapur'
    id = db.Column(db.Integer, primary_key=True)
    nama = db.Column(db.String(120), nullable=False)
    kode = db.Column(db.String(30), default='')      # kode singkat opsional
    alamat = db.Column(db.String(200), default='')
    no_wa = db.Column(db.String(50), default='')     # Nomor WA Ka SPPG / Admin Utama Dapur
    no_wa_akuntan = db.Column(db.String(50), default='')  # Nomor WA Akuntan Dapur
    notif_pengajuan_aktif = db.Column(db.Boolean, default=True)  # Toggle notif pengajuan
    notif_validasi_aktif = db.Column(db.Boolean, default=True)   # Toggle notif validasi
    nomor_va = db.Column(db.String(50), default='')  # Nomor Rekening / Virtual Account Dapur
    bank_va = db.Column(db.String(50), default='BRI') # Bank Virtual Account (BRI, BCA, Mandiri, BNI)
    saldo_va = db.Column(db.Float, default=0.0)      # Saldo VA aktif dapur
    aktif = db.Column(db.Boolean, default=True)
    urutan = db.Column(db.Integer, default=0)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def to_dict(self):
        return {
            'id': self.id,
            'nama': self.nama,
            'kode': self.kode,
            'alamat': self.alamat,
            'no_wa': self.no_wa or '',
            'no_wa_akuntan': self.no_wa_akuntan or '',
            'notif_pengajuan_aktif': bool(self.notif_pengajuan_aktif if self.notif_pengajuan_aktif is not None else True),
            'notif_validasi_aktif': bool(self.notif_validasi_aktif if self.notif_validasi_aktif is not None else True),
            'nomor_va': self.nomor_va or '',
            'bank_va': self.bank_va or 'BRI',
            'saldo_va': float(self.saldo_va or 0.0),
            'aktif': self.aktif,
            'urutan': self.urutan,
        }


class CatatanBelanjaPemasok(db.Model):
    __tablename__ = 'catatan_belanja_pemasok'
    id = db.Column(db.Integer, primary_key=True)
    pemasok_id = db.Column(db.Integer, db.ForeignKey('pemasok.id'), nullable=True)
    dapur_id = db.Column(db.Integer, db.ForeignKey('dapur.id'), nullable=True)
    tanggal = db.Column(db.Date, nullable=False)
    kategori = db.Column(db.String(100), default='')
    nama_penerima = db.Column(db.String(150), default='')
    bank_penerima = db.Column(db.String(50), default='')
    no_rekening_penerima = db.Column(db.String(50), default='')
    total_belanja = db.Column(db.Float, default=0)
    biaya_admin = db.Column(db.Float, default=0)    # potongan admin non-BRI
    total_transfer = db.Column(db.Float, default=0) # total_belanja - biaya_admin
    keterangan = db.Column(db.String(255), default='')
    diajukan = db.Column(db.Boolean, default=False)
    divalidasi = db.Column(db.Boolean, default=False)
    validated_by = db.Column(db.String(100), default='')
    validated_at = db.Column(db.DateTime, nullable=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    pemasok = db.relationship('Pemasok', backref='catatan_belanja')
    dapur = db.relationship('Dapur', backref='catatan_belanja')

    def to_dict(self):
        p_nama  = self.nama_penerima or (self.pemasok.nama if self.pemasok else '')
        p_bank  = self.bank_penerima or (self.pemasok.bank if self.pemasok else '')
        p_norek = self.no_rekening_penerima or (self.pemasok.no_rekening if self.pemasok else '')
        # Format jam & tanggal pengajuan serta validasi (WIB / UTC+7)
        wib_offset = timedelta(hours=7)
        c_at = (self.created_at + wib_offset) if self.created_at else None
        v_at = (self.validated_at + wib_offset) if self.validated_at else None

        created_at_str = c_at.strftime('%d/%m/%Y %H:%M') if c_at else ''
        created_time_str = c_at.strftime('%H:%M') if c_at else ''
        validated_at_str = v_at.strftime('%d/%m/%Y %H:%M') if v_at else ''
        validated_time_str = v_at.strftime('%H:%M') if v_at else ''
        return {
            'id': self.id,
            'pemasok_id': self.pemasok_id,
            'pemasok': self.pemasok.to_dict() if self.pemasok else None,
            'dapur_id': self.dapur_id,
            'dapur': self.dapur.to_dict() if self.dapur else None,
            'tanggal': self.tanggal.strftime('%Y-%m-%d'),
            'kategori': self.kategori or (self.pemasok.kategori if self.pemasok else ''),
            'nama_penerima': p_nama,
            'bank_penerima': p_bank,
            'no_rekening_penerima': p_norek,
            'total_belanja': self.total_belanja,
            'biaya_admin': self.biaya_admin,
            'total_transfer': self.total_transfer,
            'keterangan': self.keterangan,
            'diajukan': self.diajukan,
            'divalidasi': self.divalidasi,
            'validated_by': self.validated_by,
            'validated_at': self.validated_at.isoformat() if self.validated_at else None,
            'validated_at_str': validated_at_str,
            'validated_time_str': validated_time_str,
            'created_at': self.created_at.isoformat(),
            'created_at_str': created_at_str,
            'created_time_str': created_time_str,
        }


class Pengaturan(db.Model):
    """Key-value store untuk konfigurasi aplikasi."""
    __tablename__ = 'pengaturan'
    id = db.Column(db.Integer, primary_key=True)
    kunci = db.Column(db.String(80), unique=True, nullable=False)
    nilai = db.Column(db.Text, default='')
    deskripsi = db.Column(db.String(200), default='')

    def to_dict(self):
        return {'id': self.id, 'kunci': self.kunci,
                'nilai': self.nilai, 'deskripsi': self.deskripsi}


class KategoriPengajuan(db.Model):
    """Kategori pengajuan dinamis yang dapat diatur di menu Pengaturan."""
    __tablename__ = 'kategori_pengajuan'
    id = db.Column(db.Integer, primary_key=True)
    nama = db.Column(db.String(100), nullable=False)
    jenis = db.Column(db.String(30), default='BARANG')  # 'BARANG' atau 'OPERASIONAL'
    deskripsi = db.Column(db.String(255), default='')
    aktif = db.Column(db.Boolean, default=True)
    urutan = db.Column(db.Integer, default=0)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def to_dict(self):
        return {
            'id': self.id,
            'nama': self.nama,
            'jenis': self.jenis,
            'deskripsi': self.deskripsi,
            'aktif': self.aktif,
            'urutan': self.urutan,
        }


class PengajuanOperasional(db.Model):
    """Pengajuan biaya operasional dapur."""
    __tablename__ = 'pengajuan_operasional'
    id = db.Column(db.Integer, primary_key=True)
    nomor = db.Column(db.String(40), unique=True, nullable=False)
    dapur_id = db.Column(db.Integer, db.ForeignKey('dapur.id'), nullable=True)
    pemasok_id = db.Column(db.Integer, db.ForeignKey('pemasok.id'), nullable=True)

    nama_penerima = db.Column(db.String(150), nullable=False)
    bank_penerima = db.Column(db.String(50), nullable=False)
    no_rekening_penerima = db.Column(db.String(50), nullable=False)

    tanggal = db.Column(db.Date, nullable=False)
    kategori = db.Column(db.String(100), nullable=False)
    keperluan = db.Column(db.String(255), nullable=False)

    nominal = db.Column(db.Float, default=0)
    biaya_admin = db.Column(db.Float, default=0)
    total_transfer = db.Column(db.Float, default=0)

    status = db.Column(db.String(30), default='diajukan')  # 'draft', 'diajukan', 'divalidasi'
    validated_by = db.Column(db.String(100), default='')
    validated_at = db.Column(db.DateTime, nullable=True)
    keterangan = db.Column(db.Text, default='')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    dapur = db.relationship('Dapur', backref='operasional_list')
    pemasok = db.relationship('Pemasok', backref='operasional_list')

    def to_dict(self):
        # Format jam & tanggal pengajuan serta validasi (WIB / UTC+7)
        wib_offset = timedelta(hours=7)
        c_at = (self.created_at + wib_offset) if self.created_at else None
        v_at = (self.validated_at + wib_offset) if self.validated_at else None

        created_at_str = c_at.strftime('%d/%m/%Y %H:%M') if c_at else ''
        created_time_str = c_at.strftime('%H:%M') if c_at else ''
        validated_at_str = v_at.strftime('%d/%m/%Y %H:%M') if v_at else ''
        validated_time_str = v_at.strftime('%H:%M') if v_at else ''
        return {
            'id': self.id,
            'nomor': self.nomor,
            'dapur_id': self.dapur_id,
            'dapur': self.dapur.to_dict() if self.dapur else None,
            'pemasok_id': self.pemasok_id,
            'pemasok': self.pemasok.to_dict() if self.pemasok else None,
            'nama_penerima': self.nama_penerima,
            'bank_penerima': self.bank_penerima,
            'no_rekening_penerima': self.no_rekening_penerima,
            'tanggal': self.tanggal.strftime('%Y-%m-%d'),
            'kategori': self.kategori,
            'keperluan': self.keperluan,
            'nominal': self.nominal,
            'biaya_admin': self.biaya_admin,
            'total_transfer': self.total_transfer,
            'status': self.status,
            'validated_by': self.validated_by,
            'validated_at': self.validated_at.isoformat() if self.validated_at else None,
            'validated_at_str': validated_at_str,
            'validated_time_str': validated_time_str,
            'keterangan': self.keterangan,
            'created_at': self.created_at.isoformat(),
            'created_at_str': created_at_str,
            'created_time_str': created_time_str,
        }


class KategoriPengeluaranWajib(db.Model):
    """Kategori pengeluaran wajib & opsional (dapat ditambah/diedit mandiri)."""
    __tablename__ = 'kategori_pengeluaran_wajib'
    id = db.Column(db.Integer, primary_key=True)
    nama = db.Column(db.String(100), nullable=False, unique=True)
    sifat = db.Column(db.String(20), default='WAJIB')  # 'WAJIB' atau 'OPSIONAL'
    deskripsi = db.Column(db.String(255), default='')
    urutan = db.Column(db.Integer, default=0)
    aktif = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    def to_dict(self):
        return {
            'id': self.id,
            'nama': self.nama,
            'sifat': self.sifat or 'WAJIB',
            'deskripsi': self.deskripsi or '',
            'urutan': self.urutan or 0,
            'aktif': self.aktif
        }


class PengeluaranWajib(db.Model):
    """
    Catatan pengeluaran wajib & opsional (berdiri sendiri, NON-VA).
    Tidak memotong saldo Virtual Account dapur dan terpisah dari perputaran kas harian VA.
    """
    __tablename__ = 'pengeluaran_wajib'
    id = db.Column(db.Integer, primary_key=True)
    nomor = db.Column(db.String(50), unique=True, nullable=False)
    dapur_id = db.Column(db.Integer, db.ForeignKey('dapur.id'), nullable=True)
    kategori_id = db.Column(db.Integer, db.ForeignKey('kategori_pengeluaran_wajib.id'), nullable=True)
    kategori_nama = db.Column(db.String(100), default='')
    sifat = db.Column(db.String(20), default='WAJIB')  # 'WAJIB' atau 'OPSIONAL'
    tanggal = db.Column(db.Date, nullable=False)
    judul = db.Column(db.String(200), nullable=False)   # Contoh: Tagihan Listrik PLN Oktober, Gaji Staf
    nama_penerima = db.Column(db.String(150), default='')
    bank_penerima = db.Column(db.String(50), default='')
    no_rekening_penerima = db.Column(db.String(50), default='')
    metode_bayar = db.Column(db.String(30), default='TRANSFER') # 'TRANSFER', 'TUNAI', 'AUTODEBET'
    nominal = db.Column(db.Float, default=0.0)
    biaya_admin = db.Column(db.Float, default=0.0)
    total_bayar = db.Column(db.Float, default=0.0)
    keperluan = db.Column(db.Text, default='')
    status = db.Column(db.String(30), default='draft') # 'draft', 'disetujui'
    disetujui_oleh = db.Column(db.String(100), nullable=True)
    disetujui_pada = db.Column(db.DateTime, nullable=True)
    created_by = db.Column(db.String(100), default='Admin')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    dapur = db.relationship('Dapur', backref='pengeluaran_wajib_list')
    kategori = db.relationship('KategoriPengeluaranWajib', backref='pengeluaran_list')

    def to_dict(self):
        wib_offset = timedelta(hours=7)
        c_at = (self.created_at + wib_offset) if self.created_at else None
        v_at = (self.disetujui_pada + wib_offset) if self.disetujui_pada else None
        return {
            'id': self.id,
            'nomor': self.nomor,
            'dapur_id': self.dapur_id,
            'dapur_nama': self.dapur.nama if self.dapur else 'Kantor Pusat / Umum',
            'dapur': self.dapur.to_dict() if self.dapur else None,
            'kategori_id': self.kategori_id,
            'kategori_nama': self.kategori_nama or (self.kategori.nama if self.kategori else 'Lain-lain'),
            'sifat': self.sifat or 'WAJIB',
            'tanggal': self.tanggal.strftime('%Y-%m-%d') if hasattr(self.tanggal, 'strftime') else str(self.tanggal or ''),
            'judul': self.judul,
            'nama_penerima': self.nama_penerima or '-',
            'bank_penerima': self.bank_penerima or '',
            'no_rekening_penerima': self.no_rekening_penerima or '',
            'metode_bayar': self.metode_bayar or 'TRANSFER',
            'nominal': float(self.nominal or 0.0),
            'biaya_admin': float(self.biaya_admin or 0.0),
            'total_bayar': float(self.total_bayar or 0.0),
            'keperluan': self.keperluan or '',
            'status': self.status or 'draft',
            'disetujui_oleh': self.disetujui_oleh,
            'disetujui_pada_str': v_at.strftime('%d/%m/%Y %H:%M WIB') if hasattr(v_at, 'strftime') else str(v_at or ''),
            'created_by': self.created_by,
            'created_at_str': c_at.strftime('%d/%m/%Y %H:%M') if hasattr(c_at, 'strftime') else str(c_at or ''),
        }


class PaguAnggaran(db.Model):
    """Target Pagu Anggaran bulanan per dapur atau konsolidasi."""
    __tablename__ = 'pagu_anggaran'
    id = db.Column(db.Integer, primary_key=True)
    dapur_id = db.Column(db.Integer, db.ForeignKey('dapur.id'), nullable=True)  # None = Konsolidasi Semua Dapur
    tahun = db.Column(db.Integer, nullable=False)  # Contoh: 2026
    bulan = db.Column(db.Integer, nullable=False)  # 1 - 12
    nominal_belanja = db.Column(db.Float, default=0)      # Target belanja bahan
    nominal_operasional = db.Column(db.Float, default=0)  # Target operasional
    nominal_total = db.Column(db.Float, default=0)        # Total pagu
    keterangan = db.Column(db.String(255), default='')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    dapur = db.relationship('Dapur', backref='pagu_list')

    def to_dict(self):
        return {
            'id': self.id,
            'dapur_id': self.dapur_id,
            'dapur_nama': self.dapur.nama if self.dapur else 'Semua Unit Dapur (Konsolidasi)',
            'tahun': self.tahun,
            'bulan': self.bulan,
            'nominal_belanja': self.nominal_belanja,
            'nominal_operasional': self.nominal_operasional,
            'nominal_total': self.nominal_total,
            'keterangan': self.keterangan,
            'updated_at': self.updated_at.isoformat() if self.updated_at else ''
        }


class PaguMBG(db.Model):
    """
    Model Pagu & RAB Harian berbasis Penerima Manfaat MBG.
    Mengacu pada referensi sistem Dapur MBG:
    - Pengelompokan berdasarkan Unit Dapur & Periode
    - Rincian 12 Kategori Penerima Manfaat
    - Dual pricing tier (Harga Paket 1 & Harga Paket 2)
    - Perhitungan otomatis Total Porsi & Total RAB Harian
    """
    __tablename__ = 'pagu_mbg'
    id = db.Column(db.Integer, primary_key=True)
    dapur_id = db.Column(db.Integer, db.ForeignKey('dapur.id'), nullable=True)
    nama_dapur = db.Column(db.String(120), nullable=False)
    nama_periode = db.Column(db.String(150), nullable=False)
    tanggal_awal = db.Column(db.Date, nullable=False)
    tanggal_akhir = db.Column(db.Date, nullable=False)
    tanggal_pagu = db.Column(db.Date, nullable=False)

    harga_satuan_1 = db.Column(db.Float, default=8000.0)
    harga_satuan_2 = db.Column(db.Float, default=10000.0)
    jumlah_paket_mbg = db.Column(db.Integer, default=0)
    rab_total = db.Column(db.Float, default=0.0)

    # Simpan detail PM dan tier dalam format JSON
    detail_pm = db.Column(db.JSON, default=dict)

    waktu_input = db.Column(db.DateTime, default=datetime.utcnow)
    created_by = db.Column(db.String(100), default='')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    dapur = db.relationship('Dapur', backref='pagu_mbg_list')

    def to_dict(self):
        pm_dict = self.detail_pm if isinstance(self.detail_pm, dict) else {}
        if isinstance(self.detail_pm, str):
            try:
                import json
                pm_dict = json.loads(self.detail_pm)
            except Exception:
                pm_dict = {}

        return {
            'id': self.id,
            'dapur_id': self.dapur_id,
            'nama_dapur': self.nama_dapur,
            'nama_periode': self.nama_periode,
            'tanggal_awal': self.tanggal_awal.strftime('%Y-%m-%d') if self.tanggal_awal else '',
            'tanggal_akhir': self.tanggal_akhir.strftime('%Y-%m-%d') if self.tanggal_akhir else '',
            'tanggal_pagu': self.tanggal_pagu.strftime('%Y-%m-%d') if self.tanggal_pagu else '',
            'harga_satuan_1': self.harga_satuan_1,
            'harga_satuan_2': self.harga_satuan_2,
            'jumlah_paket_mbg': self.jumlah_paket_mbg,
            'rab_total': self.rab_total,
            'detail_pm': pm_dict,
            'waktu_input': self.waktu_input.isoformat() if self.waktu_input else '',
            'created_by': self.created_by,
            'created_at': self.created_at.isoformat() if self.created_at else '',
            'updated_at': self.updated_at.isoformat() if self.updated_at else ''
        }


class StandarHargaPasar(db.Model):
    """
    Model Standar Biaya Masukan & Monitoring Harga Pasar Bahan Pokok (Wonogiri).
    Menyimpan harga acuan/HET, batas minimum & maksimum wajar komoditas pangan
    untuk benchmarking belanja dapur MBG dan pencegahan anomali harga.
    """
    __tablename__ = 'standar_harga_pasar'
    id = db.Column(db.Integer, primary_key=True)
    nama_komoditas = db.Column(db.String(150), nullable=False)
    kategori = db.Column(db.String(50), nullable=False)  # BERAS, PROTEIN HEWANI, SAYUR, BUMBON, LAINNYA
    satuan = db.Column(db.String(30), default='Kg')      # Kg, Liter, Butir, Ikat, Tabung
    harga_pasar = db.Column(db.Float, nullable=False)    # Rata-rata harga pasar Wonogiri saat ini
    harga_min = db.Column(db.Float, default=0.0)         # Batas bawah wajar
    harga_max = db.Column(db.Float, default=0.0)         # Batas atas wajar (HET / Peringatan Mark-up)
    pasar_rujukan = db.Column(db.String(100), default='Pasar Kota Wonogiri') # Pasar Wonogiri, Pasar Krisak, Pasar Pracimantoro
    wilayah = db.Column(db.String(100), default='Kabupaten Wonogiri')
    tanggal_update = db.Column(db.Date, default=date.today)
    sumber = db.Column(db.String(150), default='Diskoperindag & Bapanas Wonogiri')
    catatan = db.Column(db.String(255), default='')
    aktif = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    def to_dict(self):
        return {
            'id': self.id,
            'nama_komoditas': self.nama_komoditas,
            'kategori': self.kategori,
            'satuan': self.satuan,
            'harga_pasar': self.harga_pasar,
            'harga_min': self.harga_min,
            'harga_max': self.harga_max,
            'pasar_rujukan': self.pasar_rujukan,
            'wilayah': self.wilayah,
            'tanggal_update': self.tanggal_update.strftime('%Y-%m-%d') if self.tanggal_update else '',
            'sumber': self.sumber,
            'catatan': self.catatan,
            'aktif': self.aktif
        }


class AuditLog(db.Model):
    """
    Model Audit Trail & Versioning Data:
    Mencatat setiap aksi (CREATE, UPDATE, DELETE, VALIDATE, RESTORE, BACKUP),
    siapa pelakunya (user/role), unit dapur, alamat IP, nilai data sebelum vs sesudah (JSON snapshot),
    serta flag apakah data tersebut dapat di-restore (Undo/Versioning).
    """
    __tablename__ = 'audit_logs'
    id = db.Column(db.Integer, primary_key=True)
    waktu = db.Column(db.DateTime, default=datetime.utcnow)
    username = db.Column(db.String(80), nullable=False)
    role = db.Column(db.String(30), default='')
    nama_lengkap = db.Column(db.String(120), default='')
    dapur_id = db.Column(db.Integer, db.ForeignKey('dapur.id'), nullable=True)
    dapur_nama = db.Column(db.String(120), default='')
    
    aksi = db.Column(db.String(50), nullable=False) # CREATE, UPDATE, DELETE, VALIDATE, RESTORE, BACKUP
    modul = db.Column(db.String(80), nullable=False) # BELANJA, OPERASIONAL, PAGU_MBG, PEMASOK, HARGA_PASAR, SYSTEM
    ref_id = db.Column(db.Integer, nullable=True)    # ID record terkait
    deskripsi = db.Column(db.String(255), default='')
    
    # Snapshot data sebelum dan sesudah untuk versioning / undo
    data_lama = db.Column(db.JSON, nullable=True)
    data_baru = db.Column(db.JSON, nullable=True)
    ip_address = db.Column(db.String(45), default='127.0.0.1')
    can_restore = db.Column(db.Boolean, default=False)
    is_restored = db.Column(db.Boolean, default=False)

    dapur = db.relationship('Dapur', backref='audit_logs')

    def to_dict(self):
        return {
            'id': self.id,
            'waktu': self.waktu.strftime('%d/%m/%Y %H:%M:%S') if self.waktu else '',
            'waktu_iso': self.waktu.strftime('%Y-%m-%d %H:%M') if self.waktu else '',
            'username': self.username,
            'role': self.role,
            'nama_lengkap': self.nama_lengkap,
            'dapur_id': self.dapur_id,
            'dapur_nama': self.dapur_nama,
            'aksi': self.aksi,
            'modul': self.modul,
            'ref_id': self.ref_id,
            'deskripsi': self.deskripsi,
            'data_lama': self.data_lama,
            'data_baru': self.data_baru,
            'ip_address': self.ip_address,
            'can_restore': self.can_restore,
            'is_restored': self.is_restored
        }


class MutasiVA(db.Model):
    """Mencatat riwayat mutasi arus kas masuk dan keluar dari Saldo Virtual Account (VA) Dapur."""
    __tablename__ = 'mutasi_va'
    id = db.Column(db.Integer, primary_key=True)
    dapur_id = db.Column(db.Integer, db.ForeignKey('dapur.id'), nullable=True)
    tanggal = db.Column(db.Date, nullable=False, default=date.today)
    waktu = db.Column(db.DateTime, default=datetime.utcnow)
    tipe = db.Column(db.String(20), nullable=False)        # 'MASUK', 'KELUAR'
    kategori = db.Column(db.String(50), nullable=False)    # 'TOPUP', 'BELANJA', 'OPERASIONAL', 'PENYESUAIAN'
    nominal = db.Column(db.Float, nullable=False, default=0.0)
    saldo_sebelum = db.Column(db.Float, default=0.0)
    saldo_sesudah = db.Column(db.Float, default=0.0)
    keterangan = db.Column(db.Text, default='')
    ref_nomor = db.Column(db.String(100), default='')
    operator = db.Column(db.String(100), default='')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    dapur = db.relationship('Dapur', backref='mutasi_va_list')

    def to_dict(self):
        wib_offset = timedelta(hours=7)
        waktu_wib = (self.waktu + wib_offset) if self.waktu else None
        return {
            'id': self.id,
            'dapur_id': self.dapur_id,
            'dapur_nama': self.dapur.nama if self.dapur else 'Unit Umum / Pusat',
            'dapur_kode': self.dapur.kode if self.dapur else 'PUSAT',
            'tanggal': self.tanggal.strftime('%Y-%m-%d') if self.tanggal else '',
            'tanggal_str': self.tanggal.strftime('%d/%m/%Y') if self.tanggal else '',
            'waktu_str': waktu_wib.strftime('%d/%m/%Y %H:%M') if waktu_wib else '',
            'tipe': self.tipe,
            'kategori': self.kategori,
            'nominal': float(self.nominal or 0.0),
            'saldo_sebelum': float(self.saldo_sebelum or 0.0),
            'saldo_sesudah': float(self.saldo_sesudah or 0.0),
            'keterangan': self.keterangan or '',
            'ref_nomor': self.ref_nomor or '',
            'operator': self.operator or ''
        }


# ─────────────────────────────────────────
# HELPERS
# ─────────────────────────────────────────

def log_audit(aksi, modul, ref_id=None, deskripsi='', data_lama=None, data_baru=None, can_restore=False):
    """
    Mencatat log audit secara komprehensif:
    aksi: CREATE, UPDATE, DELETE, VALIDATE, RESTORE, BACKUP
    modul: BELANJA, OPERASIONAL, PAGU_MBG, PEMASOK, HARGA_PASAR, SYSTEM
    """
    try:
        curr = None
        try:
            curr = get_current_user()
        except Exception:
            pass

        username = curr.username if curr else 'system'
        role = curr.role if curr else 'system'
        nama_lengkap = curr.nama_lengkap if curr else 'System Automatic'
        dapur_id = curr.dapur_id if curr and curr.dapur_id else None
        dapur_nama = curr.dapur.nama if curr and curr.dapur else ''

        ip_addr = '127.0.0.1'
        try:
            if request:
                ip_addr = request.headers.get('X-Forwarded-For', request.remote_addr or '127.0.0.1')
                if ',' in ip_addr:
                    ip_addr = ip_addr.split(',')[0].strip()
        except Exception:
            pass

        audit = AuditLog(
            username=username,
            role=role,
            nama_lengkap=nama_lengkap,
            dapur_id=dapur_id,
            dapur_nama=dapur_nama,
            aksi=aksi.upper(),
            modul=modul.upper(),
            ref_id=ref_id,
            deskripsi=deskripsi,
            data_lama=data_lama,
            data_baru=data_baru,
            ip_address=ip_addr,
            can_restore=can_restore,
            is_restored=False
        )
        db.session.add(audit)
        db.session.commit()
        return audit
    except Exception as e:
        print(f"[AUDIT LOG ERROR] Gagal mencatat audit: {e}")
        try:
            db.session.rollback()
        except Exception:
            pass
        return None


def get_kategori_list(jenis='BARANG'):
    """Ambil list string nama kategori aktif dari database atau default."""
    try:
        items = KategoriPengajuan.query.filter_by(jenis=jenis, aktif=True).order_by(
            KategoriPengajuan.urutan.asc(), KategoriPengajuan.nama.asc()).all()
        if items:
            return [k.nama for k in items]
    except Exception:
        pass

    if jenis == 'BARANG':
        return ['BERAS', 'SAYUR', 'BUAH', 'PROTEIN HEWANI', 'PROTEIN NABATI', 'BUMBON', 'LAINNYA']
    else:
        return ['Gas LPG', 'Listrik & Air', 'Bensin & Transportasi', 'Kebersihan & Sanitasi',
                'Kemasan & Wadah', 'Upah & Tenaga Kerja', 'Perlengkapan Dapur', 'Lain-lain']

# Fallback property untuk kompatibilitas
class _KategoriListProxy:
    def __iter__(self):
        return iter(get_kategori_list('BARANG'))
    def __getitem__(self, item):
        return get_kategori_list('BARANG')[item]
    def __len__(self):
        return len(get_kategori_list('BARANG'))

KATEGORI_LIST = _KategoriListProxy()

ADMIN_FEE_NON_BRI = 2500  # potongan transfer antar bank

def get_setting(kunci, default=''):
    """Ambil nilai pengaturan dari database."""
    row = Pengaturan.query.filter_by(kunci=kunci).first()
    return row.nilai if row else default

def set_setting(kunci, nilai, deskripsi=''):
    """Simpan/update pengaturan."""
    row = Pengaturan.query.filter_by(kunci=kunci).first()
    if row:
        row.nilai = nilai
    else:
        row = Pengaturan(kunci=kunci, nilai=nilai, deskripsi=deskripsi)
        db.session.add(row)
    db.session.commit()

def hitung_admin_fee(bank: str, total: float) -> tuple:
    """Return (biaya_admin, total_transfer) berdasarkan bank pemasok."""
    b = (bank or '').strip().upper()
    if b == 'BRI':
        return 0.0, total
    return float(ADMIN_FEE_NON_BRI), max(0.0, total - ADMIN_FEE_NON_BRI)

def generate_nomor():
    today = date.today()
    prefix = f"PGJ/{today.strftime('%Y%m%d')}/"
    count = PengajuanHeader.query.filter(
        PengajuanHeader.nomor.like(f"{prefix}%")
    ).count()
    return f"{prefix}{str(count + 1).zfill(3)}"

def generate_nomor_operasional():
    today = date.today()
    prefix = f"OPS/{today.strftime('%Y%m%d')}/"
    count = PengajuanOperasional.query.filter(
        PengajuanOperasional.nomor.like(f"{prefix}%")
    ).count()
    return f"{prefix}{str(count + 1).zfill(3)}"


# ─────────────────────────────────────────
# NOTIFIKASI HELPER (TELEGRAM & WHATSAPP)
# ─────────────────────────────────────────

def send_telegram_msg(token, chat_id, text_html):
    """Kirim pesan ke Telegram Bot via HTTP POST"""
    if not token or not chat_id:
        return False, "Token atau Chat ID Telegram belum diatur."
    url = f"https://api.telegram.org/bot{token}/sendMessage"
    payload = json.dumps({
        "chat_id": chat_id,
        "text": text_html,
        "parse_mode": "HTML"
    }).encode('utf-8')
    req = urllib.request.Request(
        url, data=payload,
        headers={"Content-Type": "application/json", "User-Agent": "SPPG-App/1.0"}
    )
    with urllib.request.urlopen(req, timeout=8) as resp:
        return True, "Berhasil terkirim ke Telegram"


def send_whatsapp_msg(provider, api_url, token, target, text_md):
    """Kirim pesan ke WhatsApp Gateway (Fonnte, Wablas, atau Generic Webhook)"""
    if not token or not target:
        return False, "Token API atau Nomor Tujuan WhatsApp belum diatur."

    url = (api_url or '').strip() or "https://api.fonnte.com/send"
    prov = (provider or 'fonnte').lower()

    if prov == 'fonnte':
        payload = json.dumps({
            "target": target,
            "message": text_md
        }).encode('utf-8')
        headers = {
            "Authorization": token,
            "Content-Type": "application/json",
            "User-Agent": "SPPG-App/1.0"
        }
    elif prov == 'wablas':
        payload = json.dumps({
            "phone": target,
            "message": text_md
        }).encode('utf-8')
        headers = {
            "Authorization": token,
            "Content-Type": "application/json",
            "User-Agent": "SPPG-App/1.0"
        }
    else:  # generic webhook
        payload = json.dumps({
            "target": target,
            "phone": target,
            "message": text_md,
            "text": text_md
        }).encode('utf-8')
        headers = {
            "Authorization": f"Bearer {token}" if token else "",
            "Content-Type": "application/json",
            "User-Agent": "SPPG-App/1.0"
        }

    req = urllib.request.Request(url, data=payload, headers=headers)
    with urllib.request.urlopen(req, timeout=10) as resp:
        return True, "Berhasil terkirim ke WhatsApp Gateway"


def kirim_notifikasi_sync(judul, detail_list, footer="", dapur_id=None, event_type=None):
    """
    Format dan kirim notifikasi secara sinkron berdasarkan pengaturan aktif.
    event_type: 'pengajuan' | 'validasi' | None (kirim ke semua)
    Untuk WhatsApp:
    1. Mengirim ke Admin Pusat / Bendahara (dari notif_wa_target).
    2. Jika dapur_id ditentukan, mengirim ke WA Ka SPPG & WA Akuntan Dapur
       sesuai toggle notif_pengajuan_aktif / notif_validasi_aktif per dapur.
    """
    tg_aktif = get_setting('notif_telegram_aktif', '0') == '1'
    wa_aktif = get_setting('notif_wa_aktif', '0') == '1'

    if not tg_aktif and not wa_aktif:
        return

    # Buat format WhatsApp dari template (jika ada) atau default otomatis
    def render_custom_template(tmpl_str):
        if not tmpl_str or not tmpl_str.strip():
            return None
        var_map = {
            'nama_dapur': '', 'tanggal': '', 'penerima': '', 'kategori': '',
            'nominal': '', 'keperluan': '', 'validator': '', 'waktu': '',
            'no_rek': '', 'biaya_admin': '', 'transfer': '',
            'judul': judul, 'footer': footer
        }
        for lbl, val in detail_list:
            if not lbl:
                continue
            lbl_low = lbl.lower()
            val_str = str(val)
            if 'unit' in lbl_low or 'dapur' in lbl_low:
                var_map['nama_dapur'] = val_str
            elif 'tanggal' in lbl_low:
                var_map['tanggal'] = val_str
            elif 'pemasok' in lbl_low or 'penerima' in lbl_low:
                var_map['penerima'] = val_str
            elif 'kategori' in lbl_low:
                var_map['kategori'] = val_str
            elif 'keperluan' in lbl_low or 'keterangan' in lbl_low:
                var_map['keperluan'] = val_str
            elif 'nominal' in lbl_low or 'total belanja' in lbl_low:
                var_map['nominal'] = val_str.replace('Rp ', '')
            elif 'admin' in lbl_low:
                var_map['biaya_admin'] = val_str
            elif 'transfer' in lbl_low or 'net transfer' in lbl_low:
                var_map['transfer'] = val_str
                var_map['no_rek'] = val_str
            elif 'validator' in lbl_low or 'divalidasi' in lbl_low:
                var_map['validator'] = val_str
            elif 'waktu' in lbl_low:
                var_map['waktu'] = val_str

        out = tmpl_str
        for k, v in var_map.items():
            out = out.replace(f"{{{k}}}", str(v))
        return out

    def get_configured_template(channel='wa'):
        key_prefix = 'tmpl_belanja' if 'BELANJA' in judul.upper() else ('tmpl_ops' if 'OPERASIONAL' in judul.upper() else None)
        if not key_prefix or not event_type:
            return None
        setting_key = f"{key_prefix}_{event_type}_{channel}"
        raw = get_setting(setting_key, '')
        return render_custom_template(raw)

    def build_wa_text():
        custom = get_configured_template('wa')
        if custom:
            return custom
        wa_lines = [f"*{judul}*", ""]
        for lbl, val in detail_list:
            if lbl:
                wa_lines.append(f"• *{lbl}:* {val}")
            else:
                wa_lines.append(f"{val}")
        if footer:
            wa_lines.append("")
            wa_lines.append(f"_{footer}_")
        return "\n".join(wa_lines)

    def build_tg_text():
        custom = get_configured_template('tg')
        if custom:
            return custom
        tg_lines = [f"<b>{judul}</b>\n"]
        for lbl, val in detail_list:
            if lbl:
                tg_lines.append(f"• <b>{lbl}:</b> {val}")
            else:
                tg_lines.append(f"{val}")
        if footer:
            tg_lines.append(f"\n<i>{footer}</i>")
        return "\n".join(tg_lines)

    wa_text = build_wa_text()
    tg_text = build_tg_text()

    if tg_aktif:
        tg_token = get_setting('notif_telegram_token', '')
        tg_chat_id = get_setting('notif_telegram_chat_id', '')
        if tg_token and tg_chat_id:
            try:
                ok, msg = send_telegram_msg(tg_token, tg_chat_id, tg_text)
                print(f"[NOTIF TG] Status: {ok}, {msg}")
            except Exception as e:
                print(f"[NOTIF TG ERROR] {e}")

    if wa_aktif:
        wa_prov = get_setting('notif_wa_provider', 'fonnte')
        wa_url = get_setting('notif_wa_url', 'https://api.fonnte.com/send')
        wa_tok = get_setting('notif_wa_token', '')
        pusat_target = get_setting('notif_wa_target', '').strip()

        # Kumpulkan target penerima WhatsApp (Admin Pusat + Admin Dapur terkait jika ada)
        target_list = []
        if pusat_target:
            for t in pusat_target.split(','):
                t_clean = t.strip()
                if t_clean and t_clean not in target_list:
                    target_list.append(t_clean)

        # Cek apakah ada nomor WA khusus untuk dapur ini
        if dapur_id:
            try:
                dapur_obj = Dapur.query.get(dapur_id)
                if dapur_obj:
                    # Cek toggle event
                    send_event = True
                    if event_type == 'pengajuan':
                        send_event = bool(dapur_obj.notif_pengajuan_aktif if dapur_obj.notif_pengajuan_aktif is not None else True)
                    elif event_type == 'validasi':
                        send_event = bool(dapur_obj.notif_validasi_aktif if dapur_obj.notif_validasi_aktif is not None else True)

                    if send_event:
                        # WA Ka SPPG / Admin Utama Dapur
                        if dapur_obj.no_wa:
                            for d_target in dapur_obj.no_wa.split(','):
                                d_clean = d_target.strip()
                                if d_clean and d_clean not in target_list:
                                    target_list.append(d_clean)
                        # WA Akuntan Dapur
                        if hasattr(dapur_obj, 'no_wa_akuntan') and dapur_obj.no_wa_akuntan:
                            for d_target in dapur_obj.no_wa_akuntan.split(','):
                                d_clean = d_target.strip()
                                if d_clean and d_clean not in target_list:
                                    target_list.append(d_clean)
            except Exception as e:
                print(f"[NOTIF WA DAPUR ERROR] Gagal mengambil nomor WA dapur {dapur_id}: {e}")

        if wa_tok and target_list:
            for target_num in target_list:
                try:
                    ok, msg = send_whatsapp_msg(wa_prov, wa_url, wa_tok, target_num, wa_text)
                    print(f"[NOTIF WA] Target: {target_num}, Status: {ok}, {msg}")
                except Exception as e:
                    print(f"[NOTIF WA ERROR] Target: {target_num}, Error: {e}")


def kirim_notifikasi_async(judul, detail_list, footer="", dapur_id=None, event_type=None):
    """Jalankan pengiriman pesan di background thread dengan app context Flask"""
    def _worker():
        try:
            with app.app_context():
                kirim_notifikasi_sync(judul, detail_list, footer, dapur_id=dapur_id, event_type=event_type)
        except Exception as e:
            print(f"[NOTIF ASYNC ERROR] {e}")
    threading.Thread(target=_worker, daemon=True).start()



def get_current_user():
    """Mengambil user aktif dari session jika ada."""
    uid = session.get('user_id')
    if uid:
        return User.query.get(uid)
    return None

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if not session.get('user_id'):
            flash('Silakan login terlebih dahulu.', 'warning')
            return redirect(url_for('login', next=request.path))
        return f(*args, **kwargs)
    return decorated_function

def role_required(*roles):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            user = get_current_user()
            if not user:
                flash('Silakan login terlebih dahulu.', 'warning')
                return redirect(url_for('login'))
            if user.role not in roles and 'super_admin' not in roles and user.role != 'super_admin':
                flash('Anda tidak memiliki hak akses untuk tindakan ini.', 'danger')
                return redirect(url_for('index'))
            return f(*args, **kwargs)
        return decorated_function
    return decorator


# Definisi Hak Akses Default Per Role
DEFAULT_ROLE_PERMISSIONS = {
    'super_admin': {
        'dashboard': True,
        'analitik': True,
        'analisa_scm': True,
        'pagu_mbg': True,
        'monitoring_harga': True,
        'buat_pengajuan': True,        'riwayat': True,
        'pemasok': True,
        'validasi_belanja': True,
        'operasional': True,
        'analisa_va': True,
        'audit_log': True,
        'pengaturan': True,
        'peng_identitas_dapur': True,
        'peng_daftar_dapur': True,
        'peng_kategori': True,
        'peng_notifikasi': True,
        'peng_template_notif': True,
        'peng_reset_data': True,
        'pengeluaran_wajib': True,
        'fitur_pengeluaran_wajib_tambah': True,
        'fitur_pengeluaran_wajib_validasi': True,
        'fitur_pengeluaran_wajib_hapus': True,
        'fitur_pengeluaran_wajib_kategori': True,
        'fitur_validasi': True,
        'fitur_hapus': True,
        'fitur_edit': True,
        'fitur_export': True,
        'fitur_reset_data': True,
        'topup_va': True,
    },
    'validator': {
        'dashboard': True,
        'analitik': True,
        'analisa_scm': True,
        'pagu_mbg': True,
        'monitoring_harga': True,
        'buat_pengajuan': True,
        'riwayat': True,
        'pemasok': True,
        'validasi_belanja': True,
        'operasional': True,
        'analisa_va': True,
        'audit_log': True,
        'pengaturan': False,
        'peng_identitas_dapur': False,
        'peng_daftar_dapur': False,
        'peng_kategori': False,
        'peng_notifikasi': False,
        'peng_template_notif': False,
        'peng_reset_data': False,
        'pengeluaran_wajib': True,
        'fitur_pengeluaran_wajib_tambah': True,
        'fitur_pengeluaran_wajib_validasi': True,
        'fitur_pengeluaran_wajib_hapus': False,
        'fitur_pengeluaran_wajib_kategori': True,
        'fitur_validasi': True,
        'fitur_hapus': False,
        'fitur_edit': True,
        'fitur_export': True,
        'fitur_reset_data': False,
        'topup_va': True,
    },
    'admin_dapur': {
        'dashboard': True,
        'analitik': False,
        'analisa_scm': False,
        'pagu_mbg': True,
        'monitoring_harga': True,
        'buat_pengajuan': True,
        'riwayat': True,
        'pemasok': True,
        'validasi_belanja': True,
        'operasional': True,
        'analisa_va': True,
        'audit_log': False,
        'pengaturan': False,
        'peng_identitas_dapur': False,
        'peng_daftar_dapur': False,
        'peng_kategori': False,
        'peng_notifikasi': False,
        'peng_template_notif': False,
        'peng_reset_data': False,
        'pengeluaran_wajib': True,
        'fitur_pengeluaran_wajib_tambah': True,
        'fitur_pengeluaran_wajib_validasi': False,
        'fitur_pengeluaran_wajib_hapus': False,
        'fitur_pengeluaran_wajib_kategori': False,
        'fitur_validasi': False,
        'fitur_hapus': False,
        'fitur_edit': True,
        'fitur_export': True,
        'fitur_reset_data': False,
        'topup_va': False,
    },
    'viewer': {
        'dashboard': True,
        'analitik': True,
        'analisa_scm': False,
        'pagu_mbg': True,
        'monitoring_harga': True,
        'buat_pengajuan': False,
        'riwayat': True,
        'pemasok': True,
        'validasi_belanja': True,
        'operasional': True,
        'analisa_va': True,
        'audit_log': False,
        'pengaturan': False,
        'peng_identitas_dapur': False,
        'peng_daftar_dapur': False,
        'peng_kategori': False,
        'peng_notifikasi': False,
        'peng_template_notif': False,
        'peng_reset_data': False,
        'pengeluaran_wajib': True,
        'fitur_pengeluaran_wajib_tambah': False,
        'fitur_pengeluaran_wajib_validasi': False,
        'fitur_pengeluaran_wajib_hapus': False,
        'fitur_pengeluaran_wajib_kategori': False,
        'fitur_validasi': False,
        'fitur_hapus': False,
        'fitur_edit': False,
        'fitur_export': True,
        'fitur_reset_data': False,
        'topup_va': False,
    }
}

def get_role_permissions():
    """Mengambil konfigurasi matriks hak akses role & fungsi dari database pengaturan"""
    val = get_setting('role_permissions_matrix', '')
    if val:
        try:
            saved = json.loads(val)
            # Gabungkan dengan DEFAULT_ROLE_PERMISSIONS agar izin baru otomatis memiliki default
            merged = {}
            for role, def_perms in DEFAULT_ROLE_PERMISSIONS.items():
                merged[role] = dict(def_perms)
                if role in saved and isinstance(saved[role], dict):
                    merged[role].update(saved[role])
            return merged
        except Exception:
            pass
    return DEFAULT_ROLE_PERMISSIONS

def check_permission(perm_key):
    """Cek apakah user saat ini memiliki izin untuk fitur tertentu"""
    user = get_current_user()
    if not user:
        return False
    if user.role == 'super_admin':
        return True
    matrix = get_role_permissions()
    role_perms = matrix.get(user.role, {})
    return bool(role_perms.get(perm_key, False))


# ─────────────────────────────────────────
# ROUTES – MAIN & AUTH
# ─────────────────────────────────────────

@app.before_request
def require_login_globally():
    """Wajibkan login untuk mengakses seluruh halaman dan API website."""
    # Izinkan static files, login, service worker, manifest tanpa login
    allowed_endpoints = {'login', 'logout', 'service_worker', 'manifest', 'static'}
    if request.endpoint in allowed_endpoints:
        return
    if request.path.startswith('/static/'):
        return

    # Cek session login
    if not session.get('user_id'):
        if request.path.startswith('/api/') or request.is_json:
            return jsonify({'error': 'Unauthorized', 'message': 'Silakan login terlebih dahulu'}), 401
        flash('Silakan login terlebih dahulu untuk mengakses sistem.', 'warning')
        return redirect(url_for('login', next=request.path))

@app.context_processor
def inject_global():
    """Suntikkan variabel global ke semua template."""
    user = get_current_user()
    perms = get_role_permissions()
    user_perms = perms.get(user.role, {}) if user else {}

    # Ambil data saldo VA dapur terkait atau seluruh dapur aktif
    active_dapurs = []
    user_dapur_obj = None
    total_saldo_semua_dapur = 0.0

    try:
        active_dapurs = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
        total_saldo_semua_dapur = sum(float(d.saldo_va or 0.0) for d in active_dapurs)
        if user and user.dapur_id:
            user_dapur_obj = Dapur.query.get(user.dapur_id)
    except Exception:
        pass

    return {
        'nama_dapur': get_setting('nama_dapur', 'SPPG SENDANGREJO'),
        'admin_fee_non_bri': ADMIN_FEE_NON_BRI,
        'current_user': user,
        'user_dapur_id': user.dapur_id if user else None,
        'user_dapur': user_dapur_obj,
        'user_role': user.role if user else None,
        'user_perms': user_perms,
        'active_dapurs': active_dapurs,
        'total_saldo_semua_dapur': total_saldo_semua_dapur,
        'can': lambda key: True if (user and user.role == 'super_admin') else bool(user_perms.get(key, False))
    }


@app.route('/sw.js')
def service_worker():
    return send_from_directory(os.path.join(app.root_path, 'static'), 'sw.js', mimetype='application/javascript')

@app.route('/manifest.json')
def manifest():
    return send_from_directory(os.path.join(app.root_path, 'static'), 'manifest.json', mimetype='application/manifest+json')


@app.route('/login', methods=['GET', 'POST'])
def login():
    if session.get('user_id'):
        return redirect(url_for('index'))
    
    if request.method == 'POST':
        username = request.form.get('username', '').strip().lower()
        password = request.form.get('password', '').strip()
        user = User.query.filter_by(username=username).first()
        
        if user and user.check_password(password):
            if not user.aktif:
                flash('Akun Anda dinonaktifkan. Hubungi Administrator.', 'danger')
                return render_template('login.html', username=username)
            session['user_id'] = user.id
            session['username'] = user.username
            session['role'] = user.role
            session['dapur_id'] = user.dapur_id
            flash(f'Selamat datang kembali, {user.nama_lengkap}!', 'success')
            next_url = request.args.get('next') or url_for('index')
            return redirect(next_url)
        else:
            flash('Username atau password salah.', 'danger')
            return render_template('login.html', username=username)
            
    return render_template('login.html')

@app.route('/logout')
def logout():
    session.clear()
    flash('Anda telah berhasil keluar.', 'info')
    return redirect(url_for('login'))

# ── API Manajemen Pengguna (CRUD) ──
@app.route('/api/users', methods=['GET'])
def api_users_list():
    curr = get_current_user()
    if not curr or curr.role != 'super_admin':
        return jsonify({'error': 'Unauthorized'}), 403
    users = User.query.order_by(User.role.asc(), User.nama_lengkap.asc()).all()
    return jsonify([u.to_dict() for u in users])

@app.route('/api/users', methods=['POST'])
def api_users_create():
    curr = get_current_user()
    if not curr or curr.role != 'super_admin':
        return jsonify({'error': 'Unauthorized'}), 403
    data = request.json or {}
    username = data.get('username', '').strip().lower()
    nama = data.get('nama_lengkap', '').strip()
    password = data.get('password', '').strip()
    role = data.get('role', 'admin_dapur')
    dapur_id = data.get('dapur_id')

    if not username or not nama or not password:
        return jsonify({'error': 'Username, Nama Lengkap, dan Password wajib diisi'}), 400

    if User.query.filter_by(username=username).first():
        return jsonify({'error': 'Username sudah digunakan'}), 400

    new_user = User(
        username=username,
        nama_lengkap=nama,
        role=role,
        dapur_id=int(dapur_id) if dapur_id else None,
        aktif=bool(data.get('aktif', True))
    )
    new_user.set_password(password)
    db.session.add(new_user)
    db.session.commit()
    return jsonify(new_user.to_dict()), 201

@app.route('/api/users/<int:uid>', methods=['PUT'])
def api_users_update(uid):
    curr = get_current_user()
    if not curr or curr.role != 'super_admin':
        return jsonify({'error': 'Unauthorized'}), 403
    user = User.query.get_or_404(uid)
    data = request.json or {}

    if 'nama_lengkap' in data:
        user.nama_lengkap = data['nama_lengkap'].strip()
    if 'role' in data:
        user.role = data['role']
    if 'dapur_id' in data:
        user.dapur_id = int(data['dapur_id']) if data['dapur_id'] else None
    if 'aktif' in data:
        user.aktif = bool(data['aktif'])
    if data.get('password'):
        user.set_password(data['password'].strip())

    db.session.commit()
    return jsonify(user.to_dict())

@app.route('/api/users/<int:uid>', methods=['DELETE'])
def api_users_delete(uid):
    curr = get_current_user()
    if not curr or curr.role != 'super_admin':
        return jsonify({'error': 'Unauthorized'}), 403
    if curr.id == uid:
        return jsonify({'error': 'Tidak dapat menghapus akun sendiri yang sedang login'}), 400
    user = User.query.get_or_404(uid)
    db.session.delete(user)
    db.session.commit()
    return jsonify({'ok': True})


@app.route('/manajemen-role')
def manajemen_role_view():
    """Halaman Manajemen Role Pengguna & Matriks Hak Akses Fungsi Halaman"""
    curr = get_current_user()
    if curr and curr.role != 'super_admin':
        flash('Akses ditolak: Hanya Super Admin yang dapat mengakses Manajemen Role.', 'danger')
        return redirect(url_for('index'))
    dapur_list = Dapur.query.order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    return render_template('manajemen_role.html', dapur_list=dapur_list)


@app.route('/api/role-permissions', methods=['GET'])
def api_role_permissions_get():
    """Mengambil matriks izin per role"""
    curr = get_current_user()
    if curr and curr.role != 'super_admin':
        return jsonify({'error': 'Unauthorized'}), 403
    matrix = get_role_permissions()
    return jsonify(matrix)


@app.route('/api/role-permissions', methods=['POST'])
def api_role_permissions_save():
    """Menyimpan konfigurasi matriks izin per role"""
    curr = get_current_user()
    if curr and curr.role != 'super_admin':
        return jsonify({'error': 'Unauthorized'}), 403
    data = request.json or {}
    if not isinstance(data, dict):
        return jsonify({'error': 'Data matriks tidak valid'}), 400

    set_setting('role_permissions_matrix', json.dumps(data), 'Matriks hak akses role dan fungsi halaman')

    log_audit(
        aksi='UPDATE',
        modul='SYSTEM',
        deskripsi='Memperbarui matriks hak akses peran dan fungsi halaman'
    )

    return jsonify({'success': True, 'matrix': data})


@app.route('/')
def index():
    today = date.today()
    user = get_current_user()

    total_hari_ini = db.session.query(
        db.func.sum(PengajuanHeader.total)
    ).filter(PengajuanHeader.tanggal == today).scalar() or 0

    jumlah_pengajuan = PengajuanHeader.query.filter(
        PengajuanHeader.tanggal == today
    ).count()

    rekap = db.session.query(
        PengajuanHeader.kategori,
        db.func.sum(PengajuanHeader.total).label('total')
    ).filter(PengajuanHeader.tanggal == today
    ).group_by(PengajuanHeader.kategori).all()

    pengajuan_terbaru = PengajuanHeader.query.order_by(
        PengajuanHeader.created_at.desc()
    ).limit(10).all()

    # Query Operasional Terbaru (filter per unit dapur jika user admin_dapur)
    ops_query = PengajuanOperasional.query
    if user and user.role == 'admin_dapur' and user.dapur_id:
        ops_query = ops_query.filter(
            (PengajuanOperasional.dapur_id == user.dapur_id) | (PengajuanOperasional.dapur_id == None)
        )
    operasional_terbaru = ops_query.order_by(PengajuanOperasional.created_at.desc()).limit(8).all()

    # Query Validasi Belanja Terbaru (filter per unit dapur jika user admin_dapur)
    belanja_query = CatatanBelanjaPemasok.query
    if user and user.role == 'admin_dapur' and user.dapur_id:
        belanja_query = belanja_query.filter(
            (CatatanBelanjaPemasok.dapur_id == user.dapur_id) | (CatatanBelanjaPemasok.dapur_id == None)
        )
    belanja_terbaru = belanja_query.order_by(CatatanBelanjaPemasok.created_at.desc()).limit(8).all()

    # Ringkasan nominal operasional & belanja hari ini
    ops_today_query = PengajuanOperasional.query.filter(PengajuanOperasional.tanggal == today)
    belanja_today_query = CatatanBelanjaPemasok.query.filter(CatatanBelanjaPemasok.tanggal == today)
    if user and user.role == 'admin_dapur' and user.dapur_id:
        ops_today_query = ops_today_query.filter(
            (PengajuanOperasional.dapur_id == user.dapur_id) | (PengajuanOperasional.dapur_id == None)
        )
        belanja_today_query = belanja_today_query.filter(
            (CatatanBelanjaPemasok.dapur_id == user.dapur_id) | (CatatanBelanjaPemasok.dapur_id == None)
        )

    ops_today_ids = [o.id for o in ops_today_query.all()]
    total_ops_hari_ini = db.session.query(db.func.sum(PengajuanOperasional.total_transfer)).filter(
        PengajuanOperasional.id.in_(ops_today_ids)
    ).scalar() or 0 if ops_today_ids else 0

    belanja_today_ids = [b.id for b in belanja_today_query.all()]
    total_belanja_hari_ini = db.session.query(db.func.sum(CatatanBelanjaPemasok.total_transfer)).filter(
        CatatanBelanjaPemasok.id.in_(belanja_today_ids)
    ).scalar() or 0 if belanja_today_ids else 0

    return render_template('index.html',
        total_hari_ini=total_hari_ini,
        jumlah_pengajuan=jumlah_pengajuan,
        total_ops_hari_ini=total_ops_hari_ini,
        total_belanja_hari_ini=total_belanja_hari_ini,
        rekap=rekap,
        pengajuan_terbaru=pengajuan_terbaru,
        operasional_terbaru=operasional_terbaru,
        belanja_terbaru=belanja_terbaru,
        today=today.strftime('%A, %d %B %Y'),
        today_iso=today.strftime('%Y-%m-%d')
    )


@app.route('/analitik')
def analitik():
    """Halaman Dashboard Analitik & Business Intelligence."""
    dapur_list = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    today = date.today()
    start_of_month = today.replace(day=1)
    return render_template('analitik.html',
                           dapur_list=dapur_list,
                           today_iso=today.strftime('%Y-%m-%d'),
                           start_month_iso=start_of_month.strftime('%Y-%m-%d'))


@app.route('/analisa')
def analisa_scm():
    """Halaman SCM Analytics – Intelijen & Klasifikasi Pareto Pemasok."""
    curr = get_current_user()
    dapur_list = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    today = date.today()
    start_of_month = today.replace(day=1)
    nama_dapur_app = get_setting('nama_dapur', 'SPPG SENDANGREJO')
    user_role = curr.role if curr else 'super_admin'
    user_dapur_id = curr.dapur_id if curr else None
    return render_template('analisa.html',
                           dapur_list=dapur_list,
                           nama_dapur=nama_dapur_app,
                           user_role=user_role,
                           user_dapur_id=user_dapur_id,
                           today_iso=today.strftime('%Y-%m-%d'),
                           start_month_iso=start_of_month.strftime('%Y-%m-%d'))


@app.route('/api/analisa-scm')
def api_analisa_scm():
    """
    API Analitik SCM (Supply Chain Management) – Intelijen Pemasok.
    Menggabungkan data CatatanBelanjaPemasok untuk menghasilkan:
    - KPI: Total Belanja, Total Nota, Rata-rata per Nota, Jumlah Pemasok Aktif
    - Pareto ABC Classification per Pemasok
    - Analisis Risiko: Monopoli & Single Source Dependency
    - Chart: Top Omset, Top Frekuensi, Nilai Rata-rata Terbesar, Distribusi Kategori
    - Tabel Matriks Intelijen Pemasok (sortable, searchable)
    - Audit detail transaksi per pemasok
    """
    curr = get_current_user()
    dapur_id_str = request.args.get('dapur_id', '')
    tgl_dari_str = request.args.get('tgl_dari', '')
    tgl_sampai_str = request.args.get('tgl_sampai', '')
    status_filter = request.args.get('status', 'all')

    today = date.today()
    try:
        tgl_dari = datetime.strptime(tgl_dari_str, '%Y-%m-%d').date() if tgl_dari_str else today.replace(day=1)
    except ValueError:
        tgl_dari = today.replace(day=1)
    try:
        tgl_sampai = datetime.strptime(tgl_sampai_str, '%Y-%m-%d').date() if tgl_sampai_str else today
    except ValueError:
        tgl_sampai = today

    # Paksa ke dapur user sendiri jika admin_dapur
    dapur_id = None
    if curr and curr.role == 'admin_dapur' and curr.dapur_id:
        dapur_id = curr.dapur_id
    elif dapur_id_str and dapur_id_str.isdigit():
        dapur_id = int(dapur_id_str)

    # Query catatan belanja pemasok
    q = CatatanBelanjaPemasok.query.filter(
        CatatanBelanjaPemasok.tanggal >= tgl_dari,
        CatatanBelanjaPemasok.tanggal <= tgl_sampai
    )
    if dapur_id:
        q = q.filter(CatatanBelanjaPemasok.dapur_id == dapur_id)
    if status_filter == 'validated_only':
        q = q.filter(CatatanBelanjaPemasok.divalidasi == True)

    records = q.all()

    # ── Kalkulasi per-pemasok ──
    pemasok_map = {}
    kategori_map = defaultdict(float)
    kategori_dependency_map = defaultdict(lambda: {'total': 0.0, 'suppliers': set()})
    dapur_map = defaultdict(float)
    skala_trx = {'eceran': 0, 'menengah': 0, 'grosir': 0}
    total_belanja = 0.0
    total_nota = 0

    for rec in records:
        subtotal = rec.total_belanja or 0
        kat = rec.kategori or (rec.pemasok.kategori if rec.pemasok else '') or 'Lain-lain'
        kat = kat.strip().upper() if kat else 'LAINNYA'
        nama_p = rec.pemasok.nama if rec.pemasok else (rec.nama_penerima or 'Pemasok Tidak Terdaftar')
        unit_dapur = rec.dapur.nama if rec.dapur else 'Tanpa Dapur'

        total_belanja += subtotal
        total_nota += 1

        # Klasifikasi skala transaksi
        if subtotal <= 500_000:
            skala_trx['eceran'] += 1
        elif subtotal <= 2_000_000:
            skala_trx['menengah'] += 1
        else:
            skala_trx['grosir'] += 1

        # Aggregasi per pemasok
        if nama_p not in pemasok_map:
            p_id = rec.pemasok_id
            bank = rec.pemasok.bank if rec.pemasok else rec.bank_penerima or '-'
            no_rek = rec.pemasok.no_rekening if rec.pemasok else rec.no_rekening_penerima or '-'
            pemasok_map[nama_p] = {
                'id': p_id,
                'nama': nama_p,
                'bank': bank,
                'no_rekening': no_rek,
                'kategori': kat,
                'omset': 0.0,
                'frekuensi': 0,
                'detail_transaksi': []
            }
        pemasok_map[nama_p]['omset'] += subtotal
        pemasok_map[nama_p]['frekuensi'] += 1
        pemasok_map[nama_p]['detail_transaksi'].append({
            'tanggal': rec.tanggal.strftime('%Y-%m-%d'),
            'dapur': unit_dapur,
            'kategori': kat,
            'keterangan': rec.keterangan or '-',
            'nominal': subtotal,
            'divalidasi': rec.divalidasi
        })

        kategori_map[kat] += subtotal
        kategori_dependency_map[kat]['total'] += subtotal
        kategori_dependency_map[kat]['suppliers'].add(nama_p)
        dapur_map[unit_dapur] += subtotal

    rata_nota = round(total_belanja / total_nota) if total_nota > 0 else 0

    # ── Pareto ABC Classification ──
    pemasok_list = sorted(pemasok_map.values(), key=lambda x: x['omset'], reverse=True)
    cumulative = 0.0
    for s in pemasok_list:
        s['rata_rata'] = round(s['omset'] / s['frekuensi']) if s['frekuensi'] > 0 else 0
        s['market_share'] = round((s['omset'] / total_belanja * 100), 1) if total_belanja > 0 else 0
        cumulative += s['omset']
        cumulative_pct = (cumulative / total_belanja * 100) if total_belanja > 0 else 0
        if cumulative_pct <= 80:
            s['tier'] = 'A'; s['tier_name'] = 'Kelas A (VIP)'; s['tier_color'] = '#2563eb'
        elif cumulative_pct <= 95:
            s['tier'] = 'B'; s['tier_name'] = 'Kelas B (Mid)'; s['tier_color'] = '#f59e0b'
        else:
            s['tier'] = 'C'; s['tier_name'] = 'Kelas C (Low)'; s['tier_color'] = '#64748b'
        # Remove set (not JSON-serializable), keep as string counts
        del s['detail_transaksi']

    # ── Risk Analysis ──
    risk_alerts = []
    for s in pemasok_list:
        if s['market_share'] >= 25.0:
            risk_alerts.append({
                'level': 'high',
                'type': 'monopoli',
                'pemasok': s['nama'],
                'kategori': s['kategori'],
                'share': s['market_share'],
                'nominal': s['omset']
            })
    for kat, data in kategori_dependency_map.items():
        if data['total'] > 1_000_000 and len(data['suppliers']) == 1:
            sup_name = next(iter(data['suppliers']))
            risk_alerts.append({
                'level': 'medium',
                'type': 'single_source',
                'pemasok': sup_name,
                'kategori': kat,
                'nominal': data['total']
            })

    # ── Chart Datasets ──
    top5_omset = pemasok_list[:5]
    top5_freq = sorted(pemasok_list, key=lambda x: x['frekuensi'], reverse=True)[:5]
    top5_rata = sorted(pemasok_list, key=lambda x: x['rata_rata'], reverse=True)[:5]

    kategori_sorted = sorted(kategori_map.items(), key=lambda x: x[1], reverse=True)[:6]
    dapur_sorted = sorted(dapur_map.items(), key=lambda x: x[1], reverse=True)

    return jsonify({
        'periode': {
            'tgl_dari': tgl_dari.strftime('%Y-%m-%d'),
            'tgl_sampai': tgl_sampai.strftime('%Y-%m-%d'),
        },
        'kpi': {
            'total_belanja': total_belanja,
            'total_nota': total_nota,
            'rata_nota': rata_nota,
            'jumlah_pemasok': len(pemasok_list),
        },
        'pemasok_list': pemasok_list,
        'risk_alerts': risk_alerts,
        'skala_trx': skala_trx,
        'top5_omset': top5_omset,
        'top5_freq': top5_freq,
        'top5_rata': top5_rata,
        'kategori_chart': [{'nama': k, 'total': v} for k, v in kategori_sorted],
        'dapur_chart': [{'nama': k, 'total': v} for k, v in dapur_sorted],
        'total_belanja': total_belanja,
    })


@app.route('/api/analisa-scm/audit')
def api_analisa_scm_audit():
    """
    API Audit Detail Transaksi per Pemasok untuk halaman SCM Intelijen.
    Query: nama_pemasok, dapur_id, tgl_dari, tgl_sampai, status (all|validated_only)
    """
    curr = get_current_user()
    nama_pemasok = request.args.get('nama_pemasok', '').strip()
    dapur_id_str = request.args.get('dapur_id', '')
    tgl_dari_str = request.args.get('tgl_dari', '')
    tgl_sampai_str = request.args.get('tgl_sampai', '')
    status_filter = request.args.get('status', 'all')

    today = date.today()
    try:
        tgl_dari = datetime.strptime(tgl_dari_str, '%Y-%m-%d').date() if tgl_dari_str else today.replace(day=1)
    except ValueError:
        tgl_dari = today.replace(day=1)
    try:
        tgl_sampai = datetime.strptime(tgl_sampai_str, '%Y-%m-%d').date() if tgl_sampai_str else today
    except ValueError:
        tgl_sampai = today

    # Paksa ke dapur user sendiri jika admin_dapur
    dapur_id = None
    if curr and curr.role == 'admin_dapur' and curr.dapur_id:
        dapur_id = curr.dapur_id
    elif dapur_id_str and dapur_id_str.isdigit():
        dapur_id = int(dapur_id_str)

    # Query basis waktu & dapur
    q = CatatanBelanjaPemasok.query.filter(
        CatatanBelanjaPemasok.tanggal >= tgl_dari,
        CatatanBelanjaPemasok.tanggal <= tgl_sampai
    )
    if dapur_id:
        q = q.filter(CatatanBelanjaPemasok.dapur_id == dapur_id)
    if status_filter == 'validated_only':
        q = q.filter(CatatanBelanjaPemasok.divalidasi == True)

    records = q.all()

    # Filter by nama pemasok (dari relasi atau nama_penerima)
    transaksi = []
    total_nominal = 0.0
    total_nota = 0
    for rec in records:
        nama_p = rec.pemasok.nama if rec.pemasok else (rec.nama_penerima or '')
        if nama_pemasok and nama_p.strip().lower() != nama_pemasok.lower():
            continue
        kat = rec.kategori or (rec.pemasok.kategori if rec.pemasok else '') or 'Lainnya'
        kat = kat.strip().upper() if kat else 'LAINNYA'
        unit_dapur = rec.dapur.nama if rec.dapur else 'Tanpa Dapur'
        subtotal = rec.total_belanja or 0
        total_nominal += subtotal
        total_nota += 1
        transaksi.append({
            'id': rec.id,
            'tanggal': rec.tanggal.strftime('%d/%m/%Y') if rec.tanggal else '-',
            'dapur': unit_dapur,
            'kategori': kat,
            'keterangan': rec.keterangan or '-',
            'nominal': subtotal,
            'divalidasi': rec.divalidasi,
            'bank': rec.pemasok.bank if rec.pemasok else (rec.bank_penerima or '-'),
            'no_rekening': rec.pemasok.no_rekening if rec.pemasok else (rec.no_rekening_penerima or '-'),
        })

    # Urutkan dari terbaru
    transaksi.sort(key=lambda x: x['tanggal'], reverse=True)

    return jsonify({
        'nama_pemasok': nama_pemasok,
        'periode': {
            'tgl_dari': tgl_dari.strftime('%Y-%m-%d'),
            'tgl_sampai': tgl_sampai.strftime('%Y-%m-%d'),
        },
        'transaksi': transaksi,
        'total_nominal': total_nominal,
        'total_nota': total_nota,
        'rata_rata': round(total_nominal / total_nota) if total_nota > 0 else 0,
    })


# ─────────────────────────────────────────────────────────────
# MODUL 3: MONITORING HARGA PASAR & BENCHMARKING (WONOGIRI)
# ─────────────────────────────────────────────────────────────

@app.route('/monitoring-harga')
def monitoring_harga():
    """Halaman Monitoring Harga Pasar & Standar Biaya Masukan Kabupaten Wonogiri."""
    user = get_current_user()
    dapur_list = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan).all()
    kategori_barang = get_kategori_list('BARANG')
    return render_template(
        'monitoring_harga.html',
        dapur_list=dapur_list,
        kategori_barang=kategori_barang,
        today_iso=date.today().strftime('%Y-%m-%d')
    )


@app.route('/api/standar-harga', methods=['GET', 'POST'])
def api_standar_harga():
    """GET list harga acuan Wonogiri (dengan opsi seed data default), POST tambah/update."""
    if request.method == 'GET':
        kat = request.args.get('kategori', '')
        aktif_only = request.args.get('aktif', '1') == '1'
        q = StandarHargaPasar.query
        if aktif_only:
            q = q.filter_by(aktif=True)
        if kat:
            q = q.filter_by(kategori=kat)
        items = q.order_by(StandarHargaPasar.kategori.asc(), StandarHargaPasar.nama_komoditas.asc()).all()

        # Otomatis isi data awal (Seed default harga pasar Wonogiri) jika tabel masih kosong
        if not items and not kat:
            default_wonogiri = [
                # Beras
                {'nama': 'Beras Medium C4 / IR64', 'kat': 'BERAS', 'satuan': 'Kg', 'pasar': 14000, 'min': 12500, 'max': 15500, 'sumber': 'Pasar Kota Wonogiri', 'catatan': 'Standar utama makan siang nasi MBG'},
                {'nama': 'Beras Premium Mentik Wangi', 'kat': 'BERAS', 'satuan': 'Kg', 'pasar': 15500, 'min': 14000, 'max': 17000, 'sumber': 'Pasar Kota Wonogiri', 'catatan': 'Beras pulen wangi'},
                # Protein Hewani
                {'nama': 'Daging Ayam Ras / Potong', 'kat': 'PROTEIN HEWANI', 'satuan': 'Kg', 'pasar': 38000, 'min': 34000, 'max': 42000, 'sumber': 'Pasar Kota Wonogiri', 'catatan': 'Ayam broiler bersih karkas'},
                {'nama': 'Telur Ayam Ras', 'kat': 'PROTEIN HEWANI', 'satuan': 'Kg', 'pasar': 27500, 'min': 24000, 'max': 30000, 'sumber': 'Pasar Krisak / Wonogiri', 'catatan': 'Isi 16-17 butir/kg'},
                {'nama': 'Daging Sapi Segar (Murni)', 'kat': 'PROTEIN HEWANI', 'satuan': 'Kg', 'pasar': 130000, 'min': 120000, 'max': 140000, 'sumber': 'Pasar Kota Wonogiri', 'catatan': 'Kualitas 1 untuk olahan semur/rendang'},
                {'nama': 'Ikan Lele Segar', 'kat': 'PROTEIN HEWANI', 'satuan': 'Kg', 'pasar': 25000, 'min': 22000, 'max': 28000, 'sumber': 'Peternak Lokal Wonogiri', 'catatan': 'Isi 6-8 ekor/kg'},
                {'nama': 'Ikan Nila Segar (Waduk Gajah Mungkur)', 'kat': 'PROTEIN HEWANI', 'satuan': 'Kg', 'pasar': 34000, 'min': 30000, 'max': 38000, 'sumber': 'KJA Waduk Gajah Mungkur Wonogiri', 'catatan': 'Sumber protein air tawar lokal'},
                # Protein Nabati
                {'nama': 'Tahu Putih Bersih', 'kat': 'PROTEIN NABATI', 'satuan': 'Papan', 'pasar': 35000, 'min': 30000, 'max': 40000, 'sumber': 'Pengrajin Tahu Wonogiri', 'catatan': '1 papan isi ±80-100 potong'},
                {'nama': 'Tempe Kedelai Daun/Plastik', 'kat': 'PROTEIN NABATI', 'satuan': 'Lonjor', 'pasar': 6000, 'min': 5000, 'max': 8000, 'sumber': 'Pasar Kota Wonogiri', 'catatan': 'Tempe padat higienis'},
                # Bumbon & Minyak
                {'nama': 'Minyak Goreng Curah / Minyakita', 'kat': 'BUMBON', 'satuan': 'Liter', 'pasar': 17500, 'min': 15500, 'max': 19000, 'sumber': 'Pasar Kota Wonogiri', 'catatan': 'Sesuai HET regulasi pemerintah'},
                {'nama': 'Minyak Goreng Kemasan Premium', 'kat': 'BUMBON', 'satuan': 'Liter', 'pasar': 21000, 'min': 19000, 'max': 23500, 'sumber': 'Pasar Wonogiri', 'catatan': 'Merk SunCo/Bimoli/Filma'},
                {'nama': 'Bawang Merah Lokal', 'kat': 'BUMBON', 'satuan': 'Kg', 'pasar': 32000, 'min': 26000, 'max': 42000, 'sumber': 'Pasar Wonogiri', 'catatan': 'Bawang merah kering sedang'},
                {'nama': 'Bawang Putih Kating', 'kat': 'BUMBON', 'satuan': 'Kg', 'pasar': 38000, 'min': 34000, 'max': 45000, 'sumber': 'Pasar Wonogiri', 'catatan': 'Kualitas wangi super'},
                {'nama': 'Cabai Merah Keriting', 'kat': 'BUMBON', 'satuan': 'Kg', 'pasar': 45000, 'min': 35000, 'max': 60000, 'sumber': 'Pasar Wonogiri', 'catatan': 'Fluktuatif sesuai cuaca'},
                {'nama': 'Cabai Rawit Merah (Setan)', 'kat': 'BUMBON', 'satuan': 'Kg', 'pasar': 65000, 'min': 45000, 'max': 85000, 'sumber': 'Pasar Wonogiri', 'catatan': 'Sangat fluktuatif'},
                {'nama': 'Gula Pasir Kristal Putih', 'kat': 'BUMBON', 'satuan': 'Kg', 'pasar': 17500, 'min': 16500, 'max': 18500, 'sumber': 'Pasar Wonogiri', 'catatan': 'Gula konsumsi standar'},
                {'nama': 'Garam Beryodium', 'kat': 'BUMBON', 'satuan': 'Bks (250g)', 'pasar': 3500, 'min': 2500, 'max': 5000, 'sumber': 'Pasar Wonogiri', 'catatan': 'Standar gizi yodium'},
                # Sayur & Buah
                {'nama': 'Sayur Sop Campur (Wortel, Buncis, Kol)', 'kat': 'SAYUR', 'satuan': 'Kg', 'pasar': 14000, 'min': 11000, 'max': 18000, 'sumber': 'Pasar Wonogiri', 'catatan': 'Bahan sayur bening MBG'},
                {'nama': 'Bayam Segar', 'kat': 'SAYUR', 'satuan': 'Ikat Besar', 'pasar': 3000, 'min': 2000, 'max': 4500, 'sumber': 'Petani Sayur Selogiri', 'catatan': 'Sayuran hijau kaya zat besi'},
                {'nama': 'Pisang Ambon / Cavendish', 'kat': 'BUAH', 'satuan': 'Sisir', 'pasar': 22000, 'min': 18000, 'max': 28000, 'sumber': 'Pasar Buah Wonogiri', 'catatan': 'Buah pencuci mulut gizi seimbang'},
                {'nama': 'Semangka Merah Tanpa Biji', 'kat': 'BUAH', 'satuan': 'Kg', 'pasar': 9000, 'min': 7500, 'max': 11500, 'sumber': 'Pasar Wonogiri', 'catatan': 'Buah segar potong'},
                # Energi & Operasional
                {'nama': 'Gas LPG 3 Kg (Subsidi)', 'kat': 'LAINNYA', 'satuan': 'Tabung', 'pasar': 21000, 'min': 18000, 'max': 23000, 'sumber': 'Pangkalan Resmi Wonogiri', 'catatan': 'Kebutuhan masak harian'},
                {'nama': 'Gas LPG 12 Kg (Non-Subsidi)', 'kat': 'LAINNYA', 'satuan': 'Tabung', 'pasar': 215000, 'min': 205000, 'max': 225000, 'sumber': 'Agen LPG Wonogiri', 'catatan': 'Kebutuhan kapasitas besar'}
            ]
            for row in default_wonogiri:
                item = StandarHargaPasar(
                    nama_komoditas=row['nama'],
                    kategori=row['kat'],
                    satuan=row['satuan'],
                    harga_pasar=float(row['pasar']),
                    harga_min=float(row['min']),
                    harga_max=float(row['max']),
                    pasar_rujukan=row['sumber'],
                    wilayah='Kabupaten Wonogiri',
                    sumber='Diskoperindag Wonogiri & SPPG Benchmark',
                    catatan=row['catatan'],
                    tanggal_update=date.today(),
                    aktif=True
                )
                db.session.add(item)
            db.session.commit()
            items = StandarHargaPasar.query.filter_by(aktif=True).order_by(StandarHargaPasar.kategori.asc()).all()

        return jsonify({'items': [i.to_dict() for i in items]})

    # POST (Tambah / Ubah)
    curr = get_current_user()
    if not curr or curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Hanya Administrator atau Validator yang dapat mengubah standar harga.'}), 403

    data = request.get_json() or {}
    item_id = data.get('id')
    nama = data.get('nama_komoditas', '').strip()
    kategori = data.get('kategori', '').strip().upper()
    satuan = data.get('satuan', 'Kg').strip()
    harga_pasar = float(data.get('harga_pasar') or 0)
    harga_min = float(data.get('harga_min') or (harga_pasar * 0.85))
    harga_max = float(data.get('harga_max') or (harga_pasar * 1.15))
    pasar_rujukan = data.get('pasar_rujukan', 'Pasar Kota Wonogiri').strip()
    catatan = data.get('catatan', '').strip()

    if not nama or harga_pasar <= 0:
        return jsonify({'error': 'Nama komoditas dan harga pasar wajib diisi.'}), 400

    if item_id:
        item = StandarHargaPasar.query.get(item_id)
        if not item:
            return jsonify({'error': 'Item tidak ditemukan.'}), 404
    else:
        item = StandarHargaPasar()
        db.session.add(item)

    item.nama_komoditas = nama
    item.kategori = kategori
    item.satuan = satuan
    item.harga_pasar = harga_pasar
    item.harga_min = harga_min
    item.harga_max = harga_max
    item.pasar_rujukan = pasar_rujukan
    item.catatan = catatan
    item.tanggal_update = date.today()
    item.wilayah = 'Kabupaten Wonogiri'
    item.aktif = True

    db.session.commit()
    return jsonify({'success': True, 'item': item.to_dict()})


@app.route('/api/standar-harga/<int:item_id>', methods=['DELETE'])
def api_standar_harga_delete(item_id):
    """Hapus / nonaktifkan komoditas acuan pasar."""
    curr = get_current_user()
    if not curr or curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Unauthorized'}), 403
    db.session.delete(item)
    db.session.commit()
    return jsonify({'success': True})


@app.route('/api/standar-harga/live-sync', methods=['POST'])
def api_standar_harga_live_sync():
    """
    Sinkronisasi Realtime Katalog Harga Pasar Wonogiri:
    Memperbarui timestamp tanggal_update ke hari ini, menghitung ulang
    rentang batas wajar (+/-15%), dan mensimulasikan fluktuasi harian pasar
    berdasarkan indeks inflasi pangan Wonogiri (Diskoperindag / Bapanas).
    """
    curr = get_current_user()
    if not curr or curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Hanya Administrator atau Validator yang dapat menjalankan sinkronisasi realtime.'}), 403

    import random
    items = StandarHargaPasar.query.filter_by(aktif=True).all()
    updated_count = 0

    for it in items:
        # Sedikit variasi pasar wajar harian (-1.5% s.d +2.5%) untuk simulasi pergerakan pasar hari ini jika diinginkan
        fluktuasi = random.choice([-250, 0, 0, 250, 500]) if it.kategori in ['BUMBON', 'SAYUR', 'PROTEIN HEWANI'] else 0
        new_price = max(it.harga_min, min(it.harga_max, it.harga_pasar + fluktuasi))
        it.harga_pasar = new_price
        it.harga_min = round(new_price * 0.85)
        it.harga_max = round(new_price * 1.15)
        it.tanggal_update = date.today()
        it.sumber = 'Realtime Update – Pasar Kota Wonogiri & Bapanas'
        updated_count += 1

    db.session.commit()
    return jsonify({
        'success': True,
        'message': f'Berhasil memperbarui {updated_count} komoditas pangan Wonogiri ke harga pasar realtime hari ini ({date.today().strftime("%d/%m/%Y")}).',
        'updated_count': updated_count,
        'tanggal': date.today().strftime('%Y-%m-%d')
    })


@app.route('/api/standar-harga/quick-update', methods=['POST'])
def api_standar_harga_quick_update():
    """Update cepat harga pasar per item langsung dari tabel inline / tombol sekali klik."""
    curr = get_current_user()
    if not curr or curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Unauthorized'}), 403

    data = request.get_json() or {}
    item_id = data.get('id')
    harga_baru = float(data.get('harga_pasar') or 0)
    pasar = data.get('pasar_rujukan', '').strip()

    if not item_id or harga_baru <= 0:
        return jsonify({'error': 'ID dan harga baru harus valid'}), 400

    item = StandarHargaPasar.query.get(item_id)
    if not item:
        return jsonify({'error': 'Komoditas tidak ditemukan'}), 404

    item.harga_pasar = harga_baru
    item.harga_min = round(harga_baru * 0.85)
    item.harga_max = round(harga_baru * 1.15)
    if pasar:
        item.pasar_rujukan = pasar
    item.tanggal_update = date.today()
    item.sumber = 'Input Realtime Petugas Dapur Wonogiri'
    db.session.commit()

    return jsonify({'success': True, 'item': item.to_dict()})



@app.route('/api/standar-harga/benchmarking', methods=['GET'])
def api_standar_harga_benchmarking():
    """
    Menghitung benchmarking belanja dapur aktual terhadap standar harga pasar Wonogiri.
    Mendeteksi:
    - Rata-rata deviasi biaya belanja
    - Komparasi belanja per kategori bahan (Aktif Belanja vs Standar Pasar)
    - Peringatan Anomali / Potensi Mark-up belanja bahan dapur
    """
    dapur_id_str = request.args.get('dapur_id', '')
    tgl_dari_str = request.args.get('tgl_dari', '')
    tgl_sampai_str = request.args.get('tgl_sampai', '')

    today = date.today()
    try:
        tgl_dari = datetime.strptime(tgl_dari_str, '%Y-%m-%d').date() if tgl_dari_str else today.replace(day=1)
    except ValueError:
        tgl_dari = today.replace(day=1)
    try:
        tgl_sampai = datetime.strptime(tgl_sampai_str, '%Y-%m-%d').date() if tgl_sampai_str else today
    except ValueError:
        tgl_sampai = today

    # Standar harga pasar acuan
    standar_items = StandarHargaPasar.query.filter_by(aktif=True).all()
    kat_standar_map = defaultdict(list)
    for s in standar_items:
        kat_standar_map[s.kategori].append(s.harga_pasar)

    # Rata-rata benchmark per kategori
    benchmark_kat_avg = {}
    for k, h_list in kat_standar_map.items():
        benchmark_kat_avg[k] = round(sum(h_list) / len(h_list)) if h_list else 0

    # Query catatan belanja aktual
    q = CatatanBelanjaPemasok.query.filter(
        CatatanBelanjaPemasok.tanggal >= tgl_dari,
        CatatanBelanjaPemasok.tanggal <= tgl_sampai
    )
    if dapur_id_str and dapur_id_str.isdigit():
        q = q.filter(CatatanBelanjaPemasok.dapur_id == int(dapur_id_str))

    belanja_list = q.all()
    total_belanja = sum(b.total_belanja or 0 for b in belanja_list)
    total_nota = len(belanja_list)

    # Kategori aggregasi aktual
    kat_aktual_map = defaultdict(float)
    kat_aktual_count = defaultdict(int)
    anomali_alerts = []

    for b in belanja_list:
        kat = (b.kategori or 'LAINNYA').strip().upper()
        nominal = b.total_belanja or 0
        kat_aktual_map[kat] += nominal
        kat_aktual_count[kat] += 1

        # Cek jika nominal per nota bahan melonjak drastis
        pemasok_name = b.pemasok.nama if b.pemasok else (b.nama_penerima or 'Rekanan Pasar')
        if nominal > 3_000_000:
            anomali_alerts.append({
                'level': 'warning',
                'tanggal': b.tanggal.strftime('%d/%m/%Y'),
                'kategori': kat,
                'pemasok': pemasok_name,
                'nominal': nominal,
                'keterangan': b.keterangan or '-',
                'pesan': f"Transaksi nota besar ({nominal:,.0f}) di {kat}. Periksa volume kuantum barang fisik."
            })

    # Susun perbandingan kategori
    komparasi = []
    for kat in ['BERAS', 'PROTEIN HEWANI', 'PROTEIN NABATI', 'BUMBON', 'SAYUR', 'BUAH', 'LAINNYA']:
        akt_tot = kat_aktual_map.get(kat, 0.0)
        akt_count = kat_aktual_count.get(kat, 0)
        akt_rata = round(akt_tot / akt_count) if akt_count > 0 else 0
        std_rata = benchmark_kat_avg.get(kat, 0)
        komparasi.append({
            'kategori': kat,
            'total_belanja': akt_tot,
            'jumlah_transaksi': akt_count,
            'rata_per_transaksi': akt_rata,
            'standar_harga_acuan': std_rata,
            'indeks_efisiensi': 'Efisien' if akt_tot > 0 else 'Normal'
        })

    return jsonify({
        'wilayah': 'Kabupaten Wonogiri',
        'pasar_utama': 'Pasar Kota Wonogiri & Sekitarnya',
        'periode': {
            'tgl_dari': tgl_dari.strftime('%Y-%m-%d'),
            'tgl_sampai': tgl_sampai.strftime('%Y-%m-%d'),
        },
        'kpi': {
            'total_belanja': total_belanja,
            'total_nota': total_nota,
            'jumlah_komoditas_dipantau': len(standar_items)
        },
        'komparasi': komparasi,
        'anomali_alerts': anomali_alerts,
        'standar_list': [s.to_dict() for s in standar_items]
    })


@app.route('/api/analytics/summary')
def api_analytics_summary():
    """Endpoint agregasi data analytics untuk visualisasi Chart.js & KPI metric cards."""
    tgl_dari_str = request.args.get('tgl_dari')
    tgl_sampai_str = request.args.get('tgl_sampai')
    dapur_id_str = request.args.get('dapur_id', '')
    status_filter = request.args.get('status', 'all')  # 'all' atau 'validated_only'

    today = date.today()
    try:
        tgl_dari = datetime.strptime(tgl_dari_str, '%Y-%m-%d').date() if tgl_dari_str else today.replace(day=1)
    except ValueError:
        tgl_dari = today.replace(day=1)

    try:
        tgl_sampai = datetime.strptime(tgl_sampai_str, '%Y-%m-%d').date() if tgl_sampai_str else today
    except ValueError:
        tgl_sampai = today

    if tgl_dari > tgl_sampai:
        tgl_dari, tgl_sampai = tgl_sampai, tgl_dari

    curr_user = get_current_user()
    dapur_id = None
    if curr_user and curr_user.role == 'admin_dapur' and curr_user.dapur_id:
        dapur_id = curr_user.dapur_id
    elif dapur_id_str and dapur_id_str.isdigit():
        dapur_id = int(dapur_id_str)

    # 1. Query Belanja Bahan / Pemasok
    qb = CatatanBelanjaPemasok.query.filter(
        CatatanBelanjaPemasok.tanggal >= tgl_dari,
        CatatanBelanjaPemasok.tanggal <= tgl_sampai
    )
    if dapur_id:
        qb = qb.filter(CatatanBelanjaPemasok.dapur_id == dapur_id)
    if status_filter == 'validated_only':
        qb = qb.filter(CatatanBelanjaPemasok.divalidasi == True)
    belanja_records = qb.all()

    # 2. Query Pengajuan Operasional
    qo = PengajuanOperasional.query.filter(
        PengajuanOperasional.tanggal >= tgl_dari,
        PengajuanOperasional.tanggal <= tgl_sampai
    )
    if dapur_id:
        qo = qo.filter(PengajuanOperasional.dapur_id == dapur_id)
    if status_filter == 'validated_only':
        qo = qo.filter(PengajuanOperasional.status == 'divalidasi')
    ops_records = qo.all()

    # 3. KPI Summaries
    total_belanja = sum(b.total_belanja or 0 for b in belanja_records)
    total_ops = sum(o.nominal or 0 for o in ops_records)
    grand_total = total_belanja + total_ops

    admin_belanja = sum(b.biaya_admin or 0 for b in belanja_records)
    admin_ops = sum(o.biaya_admin or 0 for o in ops_records)
    total_admin = admin_belanja + admin_ops

    transfer_belanja = sum(b.total_transfer or 0 for b in belanja_records)
    transfer_ops = sum(o.total_transfer or 0 for o in ops_records)
    total_transfer = transfer_belanja + transfer_ops

    count_belanja = len(belanja_records)
    count_ops = len(ops_records)
    total_tx = count_belanja + count_ops

    pct_belanja = round((total_belanja / grand_total * 100), 1) if grand_total > 0 else 0
    pct_ops = round((total_ops / grand_total * 100), 1) if grand_total > 0 else 0

    valid_belanja = sum(1 for b in belanja_records if b.divalidasi)
    valid_ops = sum(1 for o in ops_records if o.status == 'divalidasi')

    # 4. Tren Pengeluaran Harian (Time Series)
    day_count = (tgl_sampai - tgl_dari).days + 1
    daily_belanja = defaultdict(float)
    daily_ops = defaultdict(float)
    for b in belanja_records:
        daily_belanja[b.tanggal.strftime('%Y-%m-%d')] += (b.total_belanja or 0)
    for o in ops_records:
        daily_ops[o.tanggal.strftime('%Y-%m-%d')] += (o.nominal or 0)

    dates = []
    series_belanja = []
    series_ops = []
    series_total = []

    cur_d = tgl_dari
    while cur_d <= tgl_sampai:
        d_str = cur_d.strftime('%Y-%m-%d')
        dates.append(cur_d.strftime('%d/%m'))
        b_val = daily_belanja.get(d_str, 0)
        o_val = daily_ops.get(d_str, 0)
        series_belanja.append(round(b_val, 2))
        series_ops.append(round(o_val, 2))
        series_total.append(round(b_val + o_val, 2))
        cur_d += timedelta(days=1)

    # 5. Breakdown Kategori Belanja Bahan
    kat_belanja_map = defaultdict(lambda: {'total': 0.0, 'count': 0})
    for b in belanja_records:
        k = (b.kategori or (b.pemasok.kategori if b.pemasok else '') or 'LAINNYA').strip().upper()
        kat_belanja_map[k]['total'] += (b.total_belanja or 0)
        kat_belanja_map[k]['count'] += 1
    sorted_kat_belanja = sorted(kat_belanja_map.items(), key=lambda x: x[1]['total'], reverse=True)
    kat_belanja_labels = [k[0] for k in sorted_kat_belanja]
    kat_belanja_values = [round(k[1]['total'], 2) for k in sorted_kat_belanja]
    kat_belanja_counts = [k[1]['count'] for k in sorted_kat_belanja]

    # 6. Breakdown Kategori Biaya Operasional
    kat_ops_map = defaultdict(lambda: {'total': 0.0, 'count': 0})
    for o in ops_records:
        k = (o.kategori or 'Lain-lain').strip()
        kat_ops_map[k]['total'] += (o.nominal or 0)
        kat_ops_map[k]['count'] += 1
    sorted_kat_ops = sorted(kat_ops_map.items(), key=lambda x: x[1]['total'], reverse=True)
    kat_ops_labels = [k[0] for k in sorted_kat_ops]
    kat_ops_values = [round(k[1]['total'], 2) for k in sorted_kat_ops]
    kat_ops_counts = [k[1]['count'] for k in sorted_kat_ops]

    # 7. Komparasi Antar Dapur
    all_dapur = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    dapur_map = {d.id: {'id': d.id, 'nama': d.nama, 'belanja': 0.0, 'ops': 0.0, 'total': 0.0, 'tx': 0} for d in all_dapur}
    dapur_map[None] = {'id': None, 'nama': 'Tanpa Dapur / Umum', 'belanja': 0.0, 'ops': 0.0, 'total': 0.0, 'tx': 0}

    for b in belanja_records:
        target = b.dapur_id if b.dapur_id in dapur_map else None
        dapur_map[target]['belanja'] += (b.total_belanja or 0)
        dapur_map[target]['total'] += (b.total_belanja or 0)
        dapur_map[target]['tx'] += 1

    for o in ops_records:
        target = o.dapur_id if o.dapur_id in dapur_map else None
        dapur_map[target]['ops'] += (o.nominal or 0)
        dapur_map[target]['total'] += (o.nominal or 0)
        dapur_map[target]['tx'] += 1

    active_dapurs = [v for k, v in dapur_map.items() if (k is not None or v['total'] > 0)]
    dapur_labels = [d['nama'] for d in active_dapurs]
    dapur_belanja = [round(d['belanja'], 2) for d in active_dapurs]
    dapur_ops = [round(d['ops'], 2) for d in active_dapurs]
    dapur_totals = [round(d['total'], 2) for d in active_dapurs]

    # 8. Top 10 Rekanan / Pemasok
    spend_map = defaultdict(lambda: {'nama': '', 'tipe': '', 'bank': '', 'belanja': 0.0, 'ops': 0.0, 'total': 0.0, 'tx': 0})
    for b in belanja_records:
        pid = b.pemasok_id or f"raw_{b.nama_penerima}"
        pname = b.pemasok.nama if b.pemasok else (b.nama_penerima or 'Tanpa Rekanan')
        ptipe = b.pemasok.tipe if b.pemasok else 'BAHAN'
        pbank = b.pemasok.bank if b.pemasok else (b.bank_penerima or '-')
        spend_map[pid]['nama'] = pname
        spend_map[pid]['tipe'] = ptipe
        spend_map[pid]['bank'] = pbank
        spend_map[pid]['belanja'] += (b.total_belanja or 0)
        spend_map[pid]['total'] += (b.total_belanja or 0)
        spend_map[pid]['tx'] += 1

    for o in ops_records:
        pid = o.pemasok_id or f"raw_{o.nama_penerima}"
        pname = o.pemasok.nama if o.pemasok else (o.nama_penerima or 'Tanpa Rekanan')
        ptipe = o.pemasok.tipe if o.pemasok else 'OPERASIONAL'
        pbank = o.pemasok.bank if o.pemasok else (o.bank_penerima or '-')
        spend_map[pid]['nama'] = pname
        spend_map[pid]['tipe'] = ptipe
        spend_map[pid]['bank'] = pbank
        spend_map[pid]['ops'] += (o.nominal or 0)
        spend_map[pid]['total'] += (o.nominal or 0)
        spend_map[pid]['tx'] += 1

    top_rekanan = sorted(spend_map.values(), key=lambda x: x['total'], reverse=True)[:10]

    # 9. Pagu Anggaran vs Realisasi (Budget vs Actual)
    # Cek apakah ada data Pagu & RAB MBG Harian pada rentang tanggal terpilih
    q_mbg = PaguMBG.query.filter(
        PaguMBG.tanggal_pagu >= tgl_dari,
        PaguMBG.tanggal_pagu <= tgl_sampai
    )
    if dapur_id:
        q_mbg = q_mbg.filter(PaguMBG.dapur_id == dapur_id)
    mbg_records = q_mbg.all()

    has_mbg_pagu = len(mbg_records) > 0
    mbg_total_rab = sum(m.rab_total or 0 for m in mbg_records)
    mbg_total_porsi = sum(m.jumlah_paket_mbg or 0 for m in mbg_records)

    target_tahun = tgl_sampai.year
    target_bulan = tgl_sampai.month

    pagu_total = 0.0
    pagu_belanja = 0.0
    pagu_ops = 0.0
    tipe_pagu = 'bulanan'

    if has_mbg_pagu and mbg_total_rab > 0:
        pagu_total = mbg_total_rab
        tipe_pagu = 'mbg_harian'
    else:
        if dapur_id:
            pagu_row = PaguAnggaran.query.filter_by(dapur_id=dapur_id, tahun=target_tahun, bulan=target_bulan).first()
            if pagu_row:
                pagu_total = pagu_row.nominal_total or (pagu_row.nominal_belanja + pagu_row.nominal_operasional)
                pagu_belanja = pagu_row.nominal_belanja or 0.0
                pagu_ops = pagu_row.nominal_operasional or 0.0
        else:
            # Cek pagu konsolidasi global
            pagu_global = PaguAnggaran.query.filter_by(dapur_id=None, tahun=target_tahun, bulan=target_bulan).first()
            if pagu_global and (pagu_global.nominal_total > 0 or pagu_global.nominal_belanja > 0):
                pagu_total = pagu_global.nominal_total or (pagu_global.nominal_belanja + pagu_global.nominal_operasional)
                pagu_belanja = pagu_global.nominal_belanja or 0.0
                pagu_ops = pagu_global.nominal_operasional or 0.0
            else:
                # Akumulasi seluruh pagu dapur di bulan tersebut
                pagu_rows = PaguAnggaran.query.filter(
                    PaguAnggaran.dapur_id != None,
                    PaguAnggaran.tahun == target_tahun,
                    PaguAnggaran.bulan == target_bulan
                ).all()
                for p in pagu_rows:
                    p_tot = p.nominal_total or (p.nominal_belanja + p.nominal_operasional)
                    pagu_total += p_tot
                    pagu_belanja += (p.nominal_belanja or 0)
                    pagu_ops += (p.nominal_operasional or 0)

    sisa_anggaran = pagu_total - grand_total
    persen_serapan = round((grand_total / pagu_total * 100), 1) if pagu_total > 0 else 0.0

    if pagu_total == 0:
        status_pagu = 'belum_diatur'
        status_label = 'Pagu Belum Diatur'
        status_color = 'secondary'
    elif persen_serapan > 100:
        status_pagu = 'overbudget'
        status_label = 'Overbudget (Defisit)'
        status_color = 'danger'
    elif persen_serapan >= 90:
        status_pagu = 'kritis'
        status_label = 'Kritis (Hampir Habis)'
        status_color = 'danger'
    elif persen_serapan >= 75:
        status_pagu = 'waspada'
        status_label = 'Waspada'
        status_color = 'warning'
    else:
        status_pagu = 'aman'
        status_label = 'Aman'
        status_color = 'success'

    # Pagu per dapur untuk tabel komparasi
    mbg_map_by_dapur = defaultdict(float)
    for m in PaguMBG.query.filter(PaguMBG.tanggal_pagu >= tgl_dari, PaguMBG.tanggal_pagu <= tgl_sampai).all():
        if m.dapur_id:
            mbg_map_by_dapur[m.dapur_id] += (m.rab_total or 0)

    pagu_map_by_dapur = {}
    for p in PaguAnggaran.query.filter_by(tahun=target_tahun, bulan=target_bulan).all():
        pagu_map_by_dapur[p.dapur_id] = p.nominal_total or (p.nominal_belanja + p.nominal_operasional)

    dapur_pagus = []
    dapur_persens = []
    for d in active_dapurs:
        if d['id'] in mbg_map_by_dapur and mbg_map_by_dapur[d['id']] > 0:
            p_val = mbg_map_by_dapur[d['id']]
        else:
            p_val = pagu_map_by_dapur.get(d['id'], 0.0)

        d['pagu'] = p_val
        pct = round((d['total'] / p_val * 100), 1) if p_val > 0 else 0.0
        d['persen'] = pct
        dapur_pagus.append(round(p_val, 2))
        dapur_persens.append(pct)

    return jsonify({
        'periode': {
            'tgl_dari': tgl_dari.strftime('%Y-%m-%d'),
            'tgl_sampai': tgl_sampai.strftime('%Y-%m-%d'),
            'hari_total': day_count,
        },
        'kpi': {
            'total_belanja': total_belanja,
            'total_ops': total_ops,
            'grand_total': grand_total,
            'total_admin': total_admin,
            'total_transfer': total_transfer,
            'count_belanja': count_belanja,
            'count_ops': count_ops,
            'total_tx': total_tx,
            'pct_belanja': pct_belanja,
            'pct_ops': pct_ops,
            'valid_belanja': valid_belanja,
            'valid_ops': valid_ops,
        },
        'budget': {
            'tahun': target_tahun,
            'bulan': target_bulan,
            'pagu_total': pagu_total,
            'pagu_belanja': pagu_belanja,
            'pagu_ops': pagu_ops,
            'realisasi_total': grand_total,
            'sisa_anggaran': sisa_anggaran,
            'persen_serapan': persen_serapan,
            'status_pagu': status_pagu,
            'status_label': status_label,
            'status_color': status_color,
            'tipe_pagu': tipe_pagu,
            'has_mbg_pagu': has_mbg_pagu,
            'mbg_total_porsi': mbg_total_porsi,
            'mbg_total_rab': mbg_total_rab,
        },
        'tren': {
            'labels': dates,
            'belanja': series_belanja,
            'ops': series_ops,
            'total': series_total,
        },
        'kategori_belanja': {
            'labels': kat_belanja_labels,
            'values': kat_belanja_values,
            'counts': kat_belanja_counts,
        },
        'kategori_ops': {
            'labels': kat_ops_labels,
            'values': kat_ops_values,
            'counts': kat_ops_counts,
        },
        'komparasi_dapur': {
            'labels': dapur_labels,
            'belanja': dapur_belanja,
            'ops': dapur_ops,
            'totals': dapur_totals,
            'pagus': dapur_pagus,
            'persens': dapur_persens,
            'items': active_dapurs,
        },
        'top_rekanan': top_rekanan,
    })


# ─────────────────────────────────────────
# ROUTES – PAGU ANGGARAN (CRUD)
# ─────────────────────────────────────────

@app.route('/api/pagu', methods=['GET'])
def api_pagu_list():
    tahun = request.args.get('tahun')
    bulan = request.args.get('bulan')
    dapur_id = request.args.get('dapur_id')

    q = PaguAnggaran.query
    if tahun:
        q = q.filter_by(tahun=int(tahun))
    if bulan:
        q = q.filter_by(bulan=int(bulan))
    if dapur_id:
        if dapur_id in ['none', 'global', '0']:
            q = q.filter_by(dapur_id=None)
        else:
            q = q.filter_by(dapur_id=int(dapur_id))

    rows = q.order_by(PaguAnggaran.tahun.desc(), PaguAnggaran.bulan.desc(), PaguAnggaran.dapur_id.asc()).all()
    return jsonify([r.to_dict() for r in rows])


@app.route('/api/pagu', methods=['POST'])
def api_pagu_save():
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Hanya Validator atau Super Admin yang dapat mengatur pagu anggaran.'}), 403

    data = request.json or {}
    tahun = int(data.get('tahun') or date.today().year)
    bulan = int(data.get('bulan') or date.today().month)
    did_val = data.get('dapur_id')
    dapur_id = int(did_val) if (did_val and str(did_val) not in ['none', '0', '', 'null']) else None

    nom_belanja = float(data.get('nominal_belanja') or 0)
    nom_ops = float(data.get('nominal_operasional') or 0)
    nom_total = float(data.get('nominal_total') or 0)
    if nom_total == 0 and (nom_belanja > 0 or nom_ops > 0):
        nom_total = nom_belanja + nom_ops

    keterangan = data.get('keterangan', '').strip()

    # Upsert (Cari jika sudah ada untuk tahun/bulan/dapur yang sama)
    pagu = PaguAnggaran.query.filter_by(dapur_id=dapur_id, tahun=tahun, bulan=bulan).first()
    if not pagu:
        pagu = PaguAnggaran(dapur_id=dapur_id, tahun=tahun, bulan=bulan)
        db.session.add(pagu)

    pagu.nominal_belanja = nom_belanja
    pagu.nominal_operasional = nom_ops
    pagu.nominal_total = nom_total
    pagu.keterangan = keterangan
    db.session.commit()

    return jsonify(pagu.to_dict()), 200


@app.route('/api/pagu/<int:pid>', methods=['DELETE'])
def api_pagu_delete(pid):
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Unauthorized'}), 403
    pagu = PaguAnggaran.query.get_or_404(pid)
    db.session.delete(pagu)
    db.session.commit()
    return jsonify({'ok': True})


# ─────────────────────────────────────────
# ROUTES – PAGU & RAB MBG (HARIAN & PENERIMA MANFAAT)
# ─────────────────────────────────────────

@app.route('/pagu')
def pagu_mbg_view():
    """Halaman Manajemen Pagu & RAB MBG berbasis Penerima Manfaat."""
    curr = get_current_user()
    nama_dapur_app = get_setting('nama_dapur', 'SPPG SENDANGREJO')
    dapurs = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    user_role = curr.role if curr else 'super_admin'
    user_dapur_id = curr.dapur_id if curr else None
    user_name = curr.nama_lengkap if curr else 'Administrator'

    can_delete = user_role in ['super_admin', 'validator']

    return render_template('pagu.html',
                           nama_dapur=nama_dapur_app,
                           dapur_list=dapurs,
                           user_role=user_role,
                           user_dapur_id=user_dapur_id,
                           user_name=user_name,
                           can_delete=can_delete)


@app.route('/api/pagu-mbg', methods=['GET'])
def api_pagu_mbg_list():
    """Mengambil riwayat data pagu & RAB harian MBG."""
    curr = get_current_user()
    dapur_id = request.args.get('dapur_id')
    nama_dapur = request.args.get('nama_dapur')
    nama_periode = request.args.get('nama_periode')
    tgl_dari_str = request.args.get('tgl_dari')
    tgl_sampai_str = request.args.get('tgl_sampai')

    q = PaguMBG.query

    # Jika admin dapur, batasi ke dapur mereka
    if curr and curr.role == 'admin_dapur' and curr.dapur_id:
        q = q.filter((PaguMBG.dapur_id == curr.dapur_id) | (PaguMBG.nama_dapur == curr.dapur.nama if curr.dapur else False))
    elif dapur_id and dapur_id.isdigit():
        q = q.filter(PaguMBG.dapur_id == int(dapur_id))
    elif nama_dapur:
        q = q.filter(PaguMBG.nama_dapur == nama_dapur)

    if nama_periode:
        q = q.filter(PaguMBG.nama_periode == nama_periode)

    if tgl_dari_str:
        try:
            tgl_dari = datetime.strptime(tgl_dari_str, '%Y-%m-%d').date()
            q = q.filter(PaguMBG.tanggal_pagu >= tgl_dari)
        except ValueError:
            pass

    if tgl_sampai_str:
        try:
            tgl_sampai = datetime.strptime(tgl_sampai_str, '%Y-%m-%d').date()
            q = q.filter(PaguMBG.tanggal_pagu <= tgl_sampai)
        except ValueError:
            pass

    records = q.order_by(PaguMBG.tanggal_pagu.desc(), PaguMBG.id.desc()).all()
    return jsonify([r.to_dict() for r in records])


@app.route('/api/pagu-mbg', methods=['POST'])
def api_pagu_mbg_save():
    """Simpan atau perbarui data pagu & RAB harian MBG."""
    curr = get_current_user()
    data = request.json or {}

    nama_dapur = (data.get('nama_dapur') or '').strip()
    nama_periode = (data.get('nama_periode') or '').strip()
    tgl_awal_str = (data.get('tanggal_awal') or '').strip()
    tgl_akhir_str = (data.get('tanggal_akhir') or '').strip()
    tgl_pagu_str = (data.get('tanggal_pagu') or '').strip()

    if not nama_dapur or not nama_periode:
        return jsonify({'ok': False, 'error': 'Unit Dapur dan Nama Periode wajib diisi.'}), 400
    if not tgl_pagu_str:
        return jsonify({'ok': False, 'error': 'Tanggal Pagu wajib diisi.'}), 400

    try:
        tgl_pagu = datetime.strptime(tgl_pagu_str, '%Y-%m-%d').date()
        tgl_awal = datetime.strptime(tgl_awal_str, '%Y-%m-%d').date() if tgl_awal_str else tgl_pagu
        tgl_akhir = datetime.strptime(tgl_akhir_str, '%Y-%m-%d').date() if tgl_akhir_str else tgl_pagu
    except ValueError:
        return jsonify({'ok': False, 'error': 'Format tanggal tidak valid (harus YYYY-MM-DD).'}), 400

    # Dapatkan dapur_id dari nama jika cocok
    dapur_obj = Dapur.query.filter_by(nama=nama_dapur).first()
    dapur_id = dapur_obj.id if dapur_obj else None

    # Hak akses: admin_dapur hanya boleh untuk dapurnya sendiri
    if curr and curr.role == 'admin_dapur' and curr.dapur_id:
        if dapur_id and dapur_id != curr.dapur_id:
            return jsonify({'ok': False, 'error': 'Anda hanya berhak menginput pagu untuk unit dapur Anda sendiri.'}), 403

    pagu_id = data.get('id')
    pagu = None
    if pagu_id:
        pagu = PaguMBG.query.get(int(pagu_id))

    # Jika tidak ada id spesifik, cek apakah sudah ada pada tanggal & dapur yang sama
    if not pagu:
        pagu = PaguMBG.query.filter_by(nama_dapur=nama_dapur, tanggal_pagu=tgl_pagu).first()

    if not pagu:
        pagu = PaguMBG(
            dapur_id=dapur_id,
            nama_dapur=nama_dapur,
            nama_periode=nama_periode,
            tanggal_awal=tgl_awal,
            tanggal_akhir=tgl_akhir,
            tanggal_pagu=tgl_pagu,
            created_by=curr.username if curr else 'system'
        )
        db.session.add(pagu)
    else:
        pagu.nama_dapur = nama_dapur
        pagu.dapur_id = dapur_id
        pagu.nama_periode = nama_periode
        pagu.tanggal_awal = tgl_awal
        pagu.tanggal_akhir = tgl_akhir
        pagu.tanggal_pagu = tgl_pagu

    pagu.harga_satuan_1 = float(data.get('harga_satuan_1') or 8000)
    pagu.harga_satuan_2 = float(data.get('harga_satuan_2') or 10000)
    pagu.jumlah_paket_mbg = int(data.get('jumlah_paket_mbg') or 0)
    pagu.rab_total = float(data.get('rab_total') or 0)
    pagu.detail_pm = data.get('detail_pm') or {}
    pagu.waktu_input = datetime.utcnow()

    db.session.commit()
    return jsonify({'ok': True, 'message': 'Data pagu harian berhasil disimpan.', 'data': pagu.to_dict()})


@app.route('/api/pagu-mbg/<int:pid>', methods=['DELETE'])
def api_pagu_mbg_delete(pid):
    """Hapus data pagu harian MBG (Khusus Super Admin & Validator)."""
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'ok': False, 'error': 'Hanya Validator atau Super Admin yang dapat menghapus data pagu.'}), 403
    pagu = PaguMBG.query.get_or_404(pid)
    db.session.delete(pagu)
    db.session.commit()
    return jsonify({'ok': True, 'message': 'Data pagu berhasil dihapus.'})



# ─────────────────────────────────────────
# ROUTES – PEMASOK (CRUD)
# ─────────────────────────────────────────

@app.route('/pemasok')
def pemasok_list():
    pemasok = Pemasok.query.order_by(Pemasok.nama).all()
    pemasok_json = [p.to_dict() for p in pemasok]
    kategori_barang = get_kategori_list('BARANG')
    kategori_ops    = get_kategori_list('OPERASIONAL')
    return render_template('pemasok.html',
                           pemasok=pemasok,
                           pemasok_json=pemasok_json,
                           kategori_list=KATEGORI_LIST,
                           kategori_barang=kategori_barang,
                           kategori_ops=kategori_ops)

@app.route('/api/pemasok', methods=['GET'])
def api_pemasok_list():
    kategori = request.args.get('kategori', '')
    tipe = request.args.get('tipe', '')
    q = Pemasok.query.filter(Pemasok.aktif == True)
    if tipe:
        tipe_upper = tipe.upper()
        if tipe_upper == 'OPERASIONAL':
            # Only OPERASIONAL and SEMUA
            q = q.filter((Pemasok.tipe == 'OPERASIONAL') | (Pemasok.tipe == 'SEMUA'))
        elif tipe_upper == 'BAHAN':
            # BAHAN, SEMUA, and NULL/empty (legacy records)
            q = q.filter((Pemasok.tipe == 'BAHAN') | (Pemasok.tipe == 'SEMUA') | (Pemasok.tipe == None) | (Pemasok.tipe == ''))
        else:
            q = q.filter(Pemasok.tipe == tipe_upper)
    if kategori:
        q = q.filter(
            (Pemasok.kategori == kategori) | (Pemasok.kategori == None) | (Pemasok.kategori == '')
        )
    return jsonify([p.to_dict() for p in q.order_by(Pemasok.nama).all()])

@app.route('/api/pemasok', methods=['POST'])
def api_pemasok_create():
    data = request.json
    p = Pemasok(
        nama=data['nama'].strip(),
        bank=data['bank'].strip(),
        no_rekening=data['no_rekening'].strip(),
        kategori=data.get('kategori', '').strip() or None,
        tipe=data.get('tipe', 'BAHAN').strip().upper(),
        aktif=data.get('aktif', True)
    )
    db.session.add(p)
    db.session.commit()
    return jsonify(p.to_dict()), 201

@app.route('/api/pemasok/<int:pid>', methods=['PUT'])
def api_pemasok_update(pid):
    p = Pemasok.query.get_or_404(pid)
    data = request.json
    if 'nama' in data:
        p.nama = data['nama'].strip()
    if 'bank' in data:
        p.bank = data['bank'].strip()
    if 'no_rekening' in data:
        p.no_rekening = data['no_rekening'].strip()
    if 'kategori' in data:
        p.kategori = data.get('kategori', '').strip() or None
    if 'tipe' in data:
        p.tipe = data.get('tipe', 'BAHAN').strip().upper()
    if 'aktif' in data:
        p.aktif = data.get('aktif', True)
    db.session.commit()
    return jsonify(p.to_dict())

@app.route('/api/pemasok/<int:pid>', methods=['DELETE'])
def api_pemasok_delete(pid):
    p = Pemasok.query.get_or_404(pid)
    db.session.delete(p)
    db.session.commit()
    return jsonify({'ok': True})


# ─────────────────────────────────────────
# ROUTES – PENGAJUAN (CRUD)
# ─────────────────────────────────────────

@app.route('/pengajuan/baru')
def pengajuan_baru():
    return render_template('pengajuan_form.html',
        kategori_list=KATEGORI_LIST,
        mode='baru',
        pengajuan=None,
        pengajuan_items_json=[],
        nomor=generate_nomor(),
        today_iso=date.today().strftime('%Y-%m-%d')
    )

@app.route('/pengajuan/<int:pid>/edit')
def pengajuan_edit(pid):
    pengajuan = PengajuanHeader.query.get_or_404(pid)
    items_json = [i.to_dict() for i in pengajuan.items]
    return render_template('pengajuan_form.html',
        kategori_list=KATEGORI_LIST,
        mode='edit',
        pengajuan=pengajuan,
        pengajuan_items_json=items_json,
        nomor=pengajuan.nomor,
        today_iso=date.today().strftime('%Y-%m-%d')
    )

@app.route('/api/pengajuan', methods=['POST'])
def api_pengajuan_create():
    data = request.json
    tanggal = datetime.strptime(data['tanggal'], '%Y-%m-%d').date()

    header = PengajuanHeader(
        nomor=generate_nomor(),
        lokasi=data.get('lokasi', 'SPPG SENDANGREJO').strip(),
        tanggal=tanggal,
        kategori=data['kategori'],
        pemasok_id=data.get('pemasok_id') or None,
        status='submitted'
    )
    db.session.add(header)
    db.session.flush()

    total = 0
    for item_data in data.get('items', []):
        qty = float(item_data.get('qty', 0))
        harga = float(item_data.get('harga_satuan', 0))
        subtotal = qty * harga
        total += subtotal
        item = PengajuanItem(
            header_id=header.id,
            nama_barang=item_data['nama_barang'].strip(),
            qty=qty,
            satuan=item_data.get('satuan', '').strip(),
            harga_satuan=harga,
            subtotal=subtotal
        )
        db.session.add(item)

    header.total = total
    db.session.commit()
    return jsonify(header.to_dict()), 201

@app.route('/api/pengajuan/<int:pid>', methods=['PUT'])
def api_pengajuan_update(pid):
    header = PengajuanHeader.query.get_or_404(pid)
    data = request.json

    header.lokasi = data.get('lokasi', header.lokasi).strip()
    header.tanggal = datetime.strptime(data['tanggal'], '%Y-%m-%d').date()
    header.kategori = data['kategori']
    header.pemasok_id = data.get('pemasok_id') or None

    # hapus items lama
    PengajuanItem.query.filter_by(header_id=header.id).delete()

    total = 0
    for item_data in data.get('items', []):
        qty = float(item_data.get('qty', 0))
        harga = float(item_data.get('harga_satuan', 0))
        subtotal = qty * harga
        total += subtotal
        item = PengajuanItem(
            header_id=header.id,
            nama_barang=item_data['nama_barang'].strip(),
            qty=qty,
            satuan=item_data.get('satuan', '').strip(),
            harga_satuan=harga,
            subtotal=subtotal
        )
        db.session.add(item)

    header.total = total
    db.session.commit()
    return jsonify(header.to_dict())

@app.route('/api/pengajuan/<int:pid>', methods=['DELETE'])
def api_pengajuan_delete(pid):
    header = PengajuanHeader.query.get_or_404(pid)
    db.session.delete(header)
    db.session.commit()
    return jsonify({'ok': True})

@app.route('/api/pengajuan/<int:pid>', methods=['GET'])
def api_pengajuan_get(pid):
    header = PengajuanHeader.query.get_or_404(pid)
    return jsonify(header.to_dict())


# ─────────────────────────────────────────
# ROUTES – RIWAYAT
# ─────────────────────────────────────────

@app.route('/riwayat')
def riwayat():
    page = request.args.get('page', 1, type=int)
    tgl_dari = request.args.get('tgl_dari', '')
    tgl_sampai = request.args.get('tgl_sampai', '')
    kategori = request.args.get('kategori', '')

    q = PengajuanHeader.query
    if tgl_dari:
        q = q.filter(PengajuanHeader.tanggal >= datetime.strptime(tgl_dari, '%Y-%m-%d').date())
    if tgl_sampai:
        q = q.filter(PengajuanHeader.tanggal <= datetime.strptime(tgl_sampai, '%Y-%m-%d').date())
    if kategori:
        q = q.filter(PengajuanHeader.kategori == kategori)

    pengajuan = q.order_by(PengajuanHeader.tanggal.desc(), PengajuanHeader.created_at.desc()
                           ).paginate(page=page, per_page=20, error_out=False)

    return render_template('riwayat.html',
        pengajuan=pengajuan,
        kategori_list=KATEGORI_LIST,
        tgl_dari=tgl_dari,
        tgl_sampai=tgl_sampai,
        kategori_filter=kategori
    )

@app.route('/pengajuan/<int:pid>/detail')
def pengajuan_detail(pid):
    pengajuan = PengajuanHeader.query.get_or_404(pid)
    return render_template('pengajuan_detail.html', pengajuan=pengajuan)



# ─────────────────────────────────────────
# ROUTES – CATATAN BELANJA PEMASOK
# ─────────────────────────────────────────

@app.route('/validasi-belanja')
def belanja_pemasok():
    # Pass all pemasok; frontend groups by tipe (BAHAN optgroup + OPERASIONAL optgroup)
    pemasok_list = Pemasok.query.filter_by(aktif=True).order_by(Pemasok.nama).all()
    pemasok_json = [p.to_dict() for p in pemasok_list]
    dapur_list   = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    dapur_json   = [d.to_dict() for d in dapur_list]
    kategori_barang = get_kategori_list(jenis='BARANG')
    return render_template('belanja_pemasok.html',
                           pemasok_json=pemasok_json,
                           dapur_json=dapur_json,
                           kategori_barang=kategori_barang,
                           today_iso=date.today().strftime('%Y-%m-%d'))

@app.route('/belanja-pemasok')
@app.route('/belanja_pemasok')
@app.route('/belanja_pemasok.html')
@app.route('/templates/belanja_pemasok.html')
def belanja_pemasok_alias():
    return redirect(url_for('belanja_pemasok'))

@app.route('/api/catatan-belanja', methods=['GET'])
def api_catatan_belanja_list():
    tgl_dari   = request.args.get('tgl_dari', '')
    tgl_sampai = request.args.get('tgl_sampai', '')
    pemasok_id = request.args.get('pemasok_id', '')
    kategori   = request.args.get('kategori', '')
    status     = request.args.get('status', '')
    dapur_id   = request.args.get('dapur_id', '')

    q = CatatanBelanjaPemasok.query
    if tgl_dari:
        q = q.filter(CatatanBelanjaPemasok.tanggal >= datetime.strptime(tgl_dari, '%Y-%m-%d').date())
    if tgl_sampai:
        q = q.filter(CatatanBelanjaPemasok.tanggal <= datetime.strptime(tgl_sampai, '%Y-%m-%d').date())
    if pemasok_id:
        q = q.filter(CatatanBelanjaPemasok.pemasok_id == int(pemasok_id))
    if kategori:
        q = q.filter(CatatanBelanjaPemasok.kategori == kategori)
    if dapur_id:
        if dapur_id in ['none', '0']:
            q = q.filter(CatatanBelanjaPemasok.dapur_id == None)
        else:
            q = q.filter(CatatanBelanjaPemasok.dapur_id == int(dapur_id))
    if status == 'belum':
        q = q.filter(CatatanBelanjaPemasok.diajukan == False)
    elif status == 'diajukan':
        q = q.filter(CatatanBelanjaPemasok.diajukan == True,
                     CatatanBelanjaPemasok.divalidasi == False)
    elif status == 'divalidasi':
        q = q.filter(CatatanBelanjaPemasok.divalidasi == True)

    data = q.order_by(CatatanBelanjaPemasok.tanggal.desc(),
                      CatatanBelanjaPemasok.created_at.desc()).all()
    return jsonify([d.to_dict() for d in data])

@app.route('/api/catatan-belanja', methods=['POST'])
def api_catatan_belanja_create():
    data = request.json or {}
    total = float(data.get('total_belanja', 0) or 0)
    pid = data.get('pemasok_id')
    pemasok_obj = Pemasok.query.get(pid) if pid else None

    nama_p  = data.get('nama_penerima', '').strip() or (pemasok_obj.nama if pemasok_obj else '')
    bank_p  = data.get('bank_penerima', '').strip() or (pemasok_obj.bank if pemasok_obj else '')
    norek_p = data.get('no_rekening_penerima', '').strip() or (pemasok_obj.no_rekening if pemasok_obj else '')
    kategori = data.get('kategori', '').strip() or (pemasok_obj.kategori if pemasok_obj else '')

    biaya, transfer = hitung_admin_fee(bank_p, total)
    did = data.get('dapur_id')

    catatan = CatatanBelanjaPemasok(
        pemasok_id=int(pid) if pid else None,
        dapur_id=int(did) if did else None,
        tanggal=datetime.strptime(data['tanggal'], '%Y-%m-%d').date(),
        kategori=kategori,
        nama_penerima=nama_p,
        bank_penerima=bank_p,
        no_rekening_penerima=norek_p,
        total_belanja=total,
        biaya_admin=biaya,
        total_transfer=transfer,
        keterangan=data.get('keterangan', '').strip(),
    )
    db.session.add(catatan)
    db.session.commit()

    # Log Audit CREATE
    log_audit(
        aksi='CREATE',
        modul='BELANJA',
        ref_id=catatan.id,
        deskripsi=f"Input belanja {catatan.kategori} - {catatan.nama_penerima} (Rp {catatan.total_belanja:,.0f})",
        data_baru=catatan.to_dict()
    )

    # Trigger Notifikasi Pengajuan Baru
    if get_setting('notif_event_pengajuan', '1') == '1':
        dapur_nama = catatan.dapur.nama if catatan.dapur else get_setting('nama_dapur', 'SPPG')
        p_nama = catatan.nama_penerima or (catatan.pemasok.nama if catatan.pemasok else '-')
        kirim_notifikasi_async(
            "🔔 PENGAJUAN BELANJA BAHAN BARU",
            [
                ("Unit Dapur", dapur_nama),
                ("Tanggal", catatan.tanggal.strftime('%d/%m/%Y')),
                ("Pemasok", p_nama),
                ("Kategori", catatan.kategori or '-'),
                ("Total Belanja", f"Rp {catatan.total_belanja:,.0f}"),
                ("Potongan Admin", f"Rp {catatan.biaya_admin:,.0f}"),
                ("Net Transfer", f"Rp {catatan.total_transfer:,.0f} ({catatan.bank_penerima} - {catatan.no_rekening_penerima})"),
                ("Keterangan", catatan.keterangan or '-'),
            ],
            "Menunggu verifikasi dan validasi bendahara.",
            dapur_id=catatan.dapur_id,
            event_type='pengajuan'
        )

    return jsonify(catatan.to_dict()), 201

@app.route('/api/catatan-belanja/<int:cid>', methods=['PUT'])
def api_catatan_belanja_update(cid):
    catatan = CatatanBelanjaPemasok.query.get_or_404(cid)
    data_lama_snapshot = catatan.to_dict()
    data = request.json or {}
    if 'pemasok_id' in data:
        catatan.pemasok_id = int(data['pemasok_id']) if data['pemasok_id'] else None
    if 'dapur_id' in data:
        catatan.dapur_id = int(data['dapur_id']) if data['dapur_id'] else None
    if 'kategori' in data:
        catatan.kategori = data['kategori'].strip()
    if 'nama_penerima' in data:
        catatan.nama_penerima = data['nama_penerima'].strip()
    if 'bank_penerima' in data:
        catatan.bank_penerima = data['bank_penerima'].strip()
    if 'no_rekening_penerima' in data:
        catatan.no_rekening_penerima = data['no_rekening_penerima'].strip()
    if 'keterangan' in data:
        catatan.keterangan = data['keterangan'].strip()
    if 'tanggal' in data:
        catatan.tanggal = datetime.strptime(data['tanggal'], '%Y-%m-%d').date()
    if 'total_belanja' in data:
        catatan.total_belanja = float(data['total_belanja'])

    # Hitung ulang admin fee
    bank = catatan.bank_penerima or (catatan.pemasok.bank if catatan.pemasok else '')
    catatan.biaya_admin, catatan.total_transfer = hitung_admin_fee(bank, catatan.total_belanja)

    db.session.commit()

    # Log Audit UPDATE
    log_audit(
        aksi='UPDATE',
        modul='BELANJA',
        ref_id=catatan.id,
        deskripsi=f"Perbarui belanja {catatan.kategori} - {catatan.nama_penerima} (Rp {catatan.total_belanja:,.0f})",
        data_lama=data_lama_snapshot,
        data_baru=catatan.to_dict()
    )

    return jsonify(catatan.to_dict())

@app.route('/api/catatan-belanja/<int:cid>', methods=['DELETE'])
def api_catatan_belanja_delete(cid):
    catatan = CatatanBelanjaPemasok.query.get_or_404(cid)
    data_lama_snapshot = catatan.to_dict()
    nama_p = catatan.nama_penerima or (catatan.pemasok.nama if catatan.pemasok else 'Pemasok')
    ref_id = catatan.id
    db.session.delete(catatan)
    db.session.commit()

    # Log Audit DELETE with can_restore=True for Undo / Pulihkan Data
    log_audit(
        aksi='DELETE',
        modul='BELANJA',
        ref_id=ref_id,
        deskripsi=f"Hapus catatan belanja {data_lama_snapshot.get('kategori','')} - {nama_p} (Rp {data_lama_snapshot.get('total_belanja',0):,.0f})",
        data_lama=data_lama_snapshot,
        can_restore=True
    )

    return jsonify({'ok': True})

@app.route('/api/catatan-belanja/<int:cid>/ajukan', methods=['POST'])
def api_catatan_belanja_ajukan(cid):
    catatan = CatatanBelanjaPemasok.query.get_or_404(cid)
    catatan.diajukan = not catatan.diajukan
    if not catatan.diajukan:
        catatan.divalidasi = False
        catatan.validated_by = ''
        catatan.validated_at = None
    db.session.commit()
    return jsonify(catatan.to_dict())

@app.route('/api/catatan-belanja/<int:cid>/validasi', methods=['POST'])
def api_catatan_belanja_validasi(cid):
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Hanya Validator atau Super Admin yang berhak memvalidasi.'}), 403
    catatan = CatatanBelanjaPemasok.query.get_or_404(cid)
    data = request.json or {}
    prev_status = catatan.divalidasi
    catatan.divalidasi = not catatan.divalidasi

    # Potong / Kembalikan Saldo VA Dapur terkait
    # Pengurangan saldo VA adalah Total Belanja (biaya admin non-BRI dipotong dari total belanja dan dibebankan ke saldo VA)
    nominal_potong = float(catatan.total_belanja or 0.0)
    dapur_terkait = Dapur.query.get(catatan.dapur_id) if catatan.dapur_id else None

    if catatan.divalidasi:
        validator_name = (curr.nama_lengkap if curr else None) or data.get('validator', 'Validator')
        catatan.validated_by = validator_name
        catatan.validated_at = datetime.utcnow()
        # Jika disetujui, saldo VA dapur berkurang otomatis & catat mutasi keluar
        if dapur_terkait:
            s_seb = float(dapur_terkait.saldo_va or 0.0)
            dapur_terkait.saldo_va = s_seb - nominal_potong
            
            ket_mutasi = f"Belanja: {catatan.kategori} - {catatan.nama_penerima}"
            if catatan.biaya_admin and catatan.biaya_admin > 0:
                ket_mutasi += f" (Trf Rp {catatan.total_transfer:,.0f} + Adm Rp {catatan.biaya_admin:,.0f})"
            
            mutasi_keluar = MutasiVA(
                dapur_id=dapur_terkait.id,
                tanggal=catatan.tanggal or date.today(),
                waktu=datetime.utcnow(),
                tipe='KELUAR',
                kategori='BELANJA',
                nominal=nominal_potong,
                saldo_sebelum=s_seb,
                saldo_sesudah=dapur_terkait.saldo_va,
                keterangan=ket_mutasi,
                ref_nomor=f"BLJ-{catatan.id}",
                operator=validator_name
            )
            db.session.add(mutasi_keluar)
    else:
        catatan.validated_by = ''
        catatan.validated_at = None
        # Jika validasi dibatalkan, kembalikan saldo VA dapur & catat mutasi masuk
        if dapur_terkait:
            s_seb = float(dapur_terkait.saldo_va or 0.0)
            dapur_terkait.saldo_va = s_seb + nominal_potong
            mutasi_masuk = MutasiVA(
                dapur_id=dapur_terkait.id,
                tanggal=date.today(),
                waktu=datetime.utcnow(),
                tipe='MASUK',
                kategori='PENYESUAIAN',
                nominal=nominal_potong,
                saldo_sebelum=s_seb,
                saldo_sesudah=dapur_terkait.saldo_va,
                keterangan=f"Batal Validasi Belanja ID {catatan.id} (Rp {catatan.total_belanja:,.0f})",
                ref_nomor=f"BLJ-CANCEL-{catatan.id}",
                operator=curr.nama_lengkap if curr else 'System'
            )
            db.session.add(mutasi_masuk)

    db.session.commit()

    # Log Audit VALIDATE
    log_audit(
        aksi='VALIDATE' if catatan.divalidasi else 'UNVALIDATE',
        modul='BELANJA',
        ref_id=catatan.id,
        deskripsi=f"{'Validasi Disetujui (Potong Saldo VA Rp ' + f'{nominal_potong:,.0f}' + ')' if catatan.divalidasi else 'Batal Validasi (Kembalikan Saldo VA)'} Belanja {catatan.kategori} - {catatan.nama_penerima} (Rp {catatan.total_belanja:,.0f})",
        data_lama={'divalidasi': prev_status},
        data_baru={'divalidasi': catatan.divalidasi, 'validated_by': catatan.validated_by, 'sisa_saldo_va': dapur_terkait.saldo_va if dapur_terkait else None}
    )

    # Trigger Notifikasi Validasi Disetujui
    if catatan.divalidasi and get_setting('notif_event_validasi', '1') == '1':
        dapur_nama = catatan.dapur.nama if catatan.dapur else get_setting('nama_dapur', 'SPPG')
        p_nama = catatan.nama_penerima or (catatan.pemasok.nama if catatan.pemasok else '-')
        kirim_notifikasi_async(
            "✅ VALIDASI BELANJA BAHAN DISETUJUI",
            [
                ("Unit Dapur", dapur_nama),
                ("Penerima", p_nama),
                ("Kategori", catatan.kategori or '-'),
                ("Total Belanja (Potong VA)", f"Rp {catatan.total_belanja:,.0f}"),
                ("Potongan Admin", f"Rp {catatan.biaya_admin:,.0f}" if catatan.biaya_admin else "Rp 0 (BRI)"),
                ("Net Transfer", f"Rp {catatan.total_transfer:,.0f} ({catatan.bank_penerima} - {catatan.no_rekening_penerima})"),
                ("Divalidasi Oleh", catatan.validated_by or 'Validator'),
                ("Waktu Validasi", datetime.now().strftime('%d/%m/%Y %H:%M WIB')),
            ],
            "Pengajuan telah divalidasi dan siap diproses transfer bank.",
            dapur_id=catatan.dapur_id,
            event_type='validasi'
        )

    return jsonify(catatan.to_dict())

@app.route('/api/catatan-belanja/summary', methods=['GET'])
def api_catatan_belanja_summary():
    tgl = request.args.get('tanggal', date.today().strftime('%Y-%m-%d'))
    dapur_id = request.args.get('dapur_id', '')
    tgl_date = datetime.strptime(tgl, '%Y-%m-%d').date()

    q_sum = CatatanBelanjaPemasok.query.filter_by(tanggal=tgl_date)
    if dapur_id:
        if dapur_id in ['none', '0']:
            q_sum = q_sum.filter_by(dapur_id=None)
        else:
            q_sum = q_sum.filter_by(dapur_id=int(dapur_id))

    rows = q_sum.all()
    total = sum(r.total_belanja for r in rows)
    diajukan = sum(1 for r in rows if r.diajukan)
    divalidasi = sum(1 for r in rows if r.divalidasi)
    return jsonify({
        'tanggal': tgl,
        'total_catatan': len(rows),
        'total_belanja': total,
        'diajukan': diajukan,
        'divalidasi': divalidasi,
        'belum': len(rows) - diajukan,
    })


# ─────────────────────────────────────────
# ROUTES – DAPUR (CRUD)
# ─────────────────────────────────────────

@app.route('/api/dapur', methods=['GET'])
def api_dapur_list():
    only_active = request.args.get('aktif', '')
    q = Dapur.query
    if only_active in ['1', 'true', 'True']:
        q = q.filter_by(aktif=True)
    items = q.order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    return jsonify([d.to_dict() for d in items])

@app.route('/api/dapur', methods=['POST'])
def api_dapur_create():
    data = request.json or {}
    nama = data.get('nama', '').strip()
    if not nama:
        return jsonify({'error': 'Nama dapur wajib diisi'}), 400
    dapur = Dapur(
        nama=nama,
        kode=data.get('kode', '').strip(),
        alamat=data.get('alamat', '').strip(),
        no_wa=data.get('no_wa', '').strip(),
        no_wa_akuntan=data.get('no_wa_akuntan', '').strip(),
        notif_pengajuan_aktif=bool(data.get('notif_pengajuan_aktif', True)),
        notif_validasi_aktif=bool(data.get('notif_validasi_aktif', True)),
        nomor_va=data.get('nomor_va', '').strip(),
        bank_va=data.get('bank_va', 'BRI').strip(),
        saldo_va=float(data.get('saldo_va') or 0.0),
        aktif=bool(data.get('aktif', True)),
        urutan=int(data.get('urutan') or 0)
    )
    db.session.add(dapur)
    db.session.commit()
    return jsonify(dapur.to_dict()), 201

@app.route('/api/dapur/<int:did>', methods=['PUT'])
def api_dapur_update(did):
    dapur = Dapur.query.get_or_404(did)
    data = request.json or {}
    if 'nama' in data:
        nama = data.get('nama', '').strip()
        if not nama:
            return jsonify({'error': 'Nama dapur tidak boleh kosong'}), 400
        dapur.nama = nama
    if 'kode' in data:
        dapur.kode = data.get('kode', '').strip()
    if 'alamat' in data:
        dapur.alamat = data.get('alamat', '').strip()
    if 'no_wa' in data:
        dapur.no_wa = data.get('no_wa', '').strip()
    if 'no_wa_akuntan' in data:
        dapur.no_wa_akuntan = data.get('no_wa_akuntan', '').strip()
    if 'notif_pengajuan_aktif' in data:
        dapur.notif_pengajuan_aktif = bool(data.get('notif_pengajuan_aktif'))
    if 'notif_validasi_aktif' in data:
        dapur.notif_validasi_aktif = bool(data.get('notif_validasi_aktif'))
    if 'nomor_va' in data:
        dapur.nomor_va = data.get('nomor_va', '').strip()
    if 'bank_va' in data:
        dapur.bank_va = data.get('bank_va', 'BRI').strip()
    if 'saldo_va' in data:
        saldo_baru = float(data.get('saldo_va') or 0.0)
        selisih = saldo_baru - float(dapur.saldo_va or 0.0)
        if abs(selisih) > 0.01:
            mutasi = MutasiVA(
                dapur_id=dapur.id,
                tanggal=date.today(),
                waktu=datetime.utcnow(),
                tipe='MASUK' if selisih > 0 else 'KELUAR',
                kategori='TOPUP' if selisih > 0 else 'PENYESUAIAN',
                nominal=abs(selisih),
                saldo_sebelum=float(dapur.saldo_va or 0.0),
                saldo_sesudah=saldo_baru,
                keterangan='Penyesuaian Saldo VA via Pengaturan',
                ref_nomor='ADJ-PENGATURAN',
                operator=get_current_user().nama_lengkap if get_current_user() else 'Admin'
            )
            db.session.add(mutasi)
        dapur.saldo_va = saldo_baru
    if 'aktif' in data:
        dapur.aktif = bool(data.get('aktif'))
    if 'urutan' in data:
        dapur.urutan = int(data.get('urutan') or 0)
    db.session.commit()
    return jsonify(dapur.to_dict())

@app.route('/api/dapur/<int:did>', methods=['DELETE'])
def api_dapur_delete(did):
    dapur = Dapur.query.get_or_404(did)
    CatatanBelanjaPemasok.query.filter_by(dapur_id=did).update({'dapur_id': None})
    PengajuanOperasional.query.filter_by(dapur_id=did).update({'dapur_id': None})
    db.session.delete(dapur)
    db.session.commit()
    return jsonify({'ok': True})

@app.route('/api/dapur/<int:did>/topup-va', methods=['POST'])
def api_dapur_topup_va(did):
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Hanya Super Admin atau Validator yang berhak melakukan Top-Up VA.'}), 403
    dapur = Dapur.query.get_or_404(did)
    data = request.json or {}
    nominal = float(data.get('nominal') or 0.0)
    if nominal <= 0:
        return jsonify({'error': 'Nominal Top-Up harus lebih dari Rp 0'}), 400

    tgl_str = data.get('tanggal')
    try:
        tgl = datetime.strptime(tgl_str, '%Y-%m-%d').date() if tgl_str else date.today()
    except Exception:
        tgl = date.today()

    saldo_lama = float(dapur.saldo_va or 0.0)
    dapur.saldo_va = saldo_lama + nominal

    ket = (data.get('keterangan') or '').strip() or f"Top-Up Saldo VA {dapur.nama}"
    ref = (data.get('ref_nomor') or '').strip() or f"TOPUP-{dapur.kode or dapur.id}-{int(datetime.utcnow().timestamp())}"
    op_name = (curr.nama_lengkap if curr else None) or 'Admin'

    mutasi = MutasiVA(
        dapur_id=dapur.id,
        tanggal=tgl,
        waktu=datetime.utcnow(),
        tipe='MASUK',
        kategori='TOPUP',
        nominal=nominal,
        saldo_sebelum=saldo_lama,
        saldo_sesudah=dapur.saldo_va,
        keterangan=ket,
        ref_nomor=ref,
        operator=op_name
    )
    db.session.add(mutasi)
    db.session.commit()

    log_audit(
        aksi='TOPUP',
        modul='VA',
        ref_id=dapur.id,
        deskripsi=f"Top-Up Saldo VA Dapur {dapur.nama} sebesar Rp {nominal:,.0f} (Saldo Baru: Rp {dapur.saldo_va:,.0f})",
        data_lama={'saldo_va': saldo_lama},
        data_baru={'saldo_va': dapur.saldo_va, 'ref_nomor': ref}
    )

    return jsonify({
        'ok': True,
        'message': f'Berhasil Top-Up Rp {nominal:,.0f} ke {dapur.nama}',
        'dapur': dapur.to_dict(),
        'mutasi': mutasi.to_dict()
    })


# ─────────────────────────────────────────
# ROUTES – ANALISA SALDO VA & MONITORING KAS
# ─────────────────────────────────────────

@app.route('/analisa-saldo-va')
@app.route('/analisa-va')
@app.route('/saldo-va')
def analisa_saldo_va():
    dapurs = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    dapur_json = [d.to_dict() for d in dapurs]
    today_iso = date.today().strftime('%Y-%m-%d')
    return render_template('analisa_saldo_va.html',
                           dapur_json=dapur_json,
                           dapurs=dapurs,
                           today_iso=today_iso)


@app.route('/api/analisa-va', methods=['GET'])
def api_analisa_va():
    dapur_id_str = request.args.get('dapur_id', '').strip()
    tgl_dari_str = request.args.get('tgl_dari', '').strip()
    tgl_sampai_str = request.args.get('tgl_sampai', '').strip()

    filter_dapur_id = int(dapur_id_str) if (dapur_id_str and dapur_id_str.isdigit()) else None

    # Tentukan rentang tanggal
    tgl_dari = None
    tgl_sampai = None
    try:
        if tgl_dari_str:
            tgl_dari = datetime.strptime(tgl_dari_str, '%Y-%m-%d').date()
    except Exception:
        pass
    try:
        if tgl_sampai_str:
            tgl_sampai = datetime.strptime(tgl_sampai_str, '%Y-%m-%d').date()
    except Exception:
        pass

    # Ambil Dapur aktif
    q_dapur = Dapur.query.filter_by(aktif=True)
    if filter_dapur_id:
        q_dapur = q_dapur.filter_by(id=filter_dapur_id)
    dapurs = q_dapur.order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()

    # Query Belanja Valid
    q_blj = CatatanBelanjaPemasok.query.filter_by(divalidasi=True)
    if filter_dapur_id:
        q_blj = q_blj.filter_by(dapur_id=filter_dapur_id)
    if tgl_dari:
        q_blj = q_blj.filter(CatatanBelanjaPemasok.tanggal >= tgl_dari)
    if tgl_sampai:
        q_blj = q_blj.filter(CatatanBelanjaPemasok.tanggal <= tgl_sampai)
    all_blj = q_blj.all()

    # Query Operasional Valid
    q_ops = PengajuanOperasional.query.filter_by(status='divalidasi')
    if filter_dapur_id:
        q_ops = q_ops.filter_by(dapur_id=filter_dapur_id)
    if tgl_dari:
        q_ops = q_ops.filter(PengajuanOperasional.tanggal >= tgl_dari)
    if tgl_sampai:
        q_ops = q_ops.filter(PengajuanOperasional.tanggal <= tgl_sampai)
    all_ops = q_ops.all()

    # Query Pending Belanja & Ops (menunggu validasi)
    q_blj_p = CatatanBelanjaPemasok.query.filter_by(divalidasi=False, diajukan=True)
    q_ops_p = PengajuanOperasional.query.filter_by(status='diajukan')
    if filter_dapur_id:
        q_blj_p = q_blj_p.filter_by(dapur_id=filter_dapur_id)
        q_ops_p = q_ops_p.filter_by(dapur_id=filter_dapur_id)
    pending_blj_total = sum(float(b.total_belanja or 0) for b in q_blj_p.all())
    pending_ops_total = sum(float(o.nominal or 0) for o in q_ops_p.all())
    total_pending = pending_blj_total + pending_ops_total

    # Query Mutasi VA
    q_mutasi = MutasiVA.query
    if filter_dapur_id:
        q_mutasi = q_mutasi.filter_by(dapur_id=filter_dapur_id)
    if tgl_dari:
        q_mutasi = q_mutasi.filter(MutasiVA.tanggal >= tgl_dari)
    if tgl_sampai:
        q_mutasi = q_mutasi.filter(MutasiVA.tanggal <= tgl_sampai)
    mutasi_list = q_mutasi.order_by(MutasiVA.tanggal.desc(), MutasiVA.id.desc()).limit(200).all()

    # Hitung Rekap Dapur (Total Pengeluaran VA = Total Belanja + Nominal Operasional)
    total_saldo_semua = sum(float(d.saldo_va or 0.0) for d in dapurs)
    total_belanja_valid = sum(float(b.total_belanja or 0.0) for b in all_blj)
    total_ops_valid = sum(float(o.nominal or 0.0) for o in all_ops)
    total_pengeluaran = total_belanja_valid + total_ops_valid

    # Hitung Burn Rate & Runway per Dapur
    dapur_analysis = []
    jumlah_kritis = 0
    jumlah_waspada = 0

    for d in dapurs:
        d_blj = sum(float(b.total_belanja or 0) for b in all_blj if b.dapur_id == d.id)
        d_ops = sum(float(o.nominal or 0) for o in all_ops if o.dapur_id == d.id)
        d_total_keluar = d_blj + d_ops

        # Pending untuk dapur ini
        d_p_blj = sum(float(b.total_belanja or 0) for b in q_blj_p.all() if b.dapur_id == d.id)
        d_p_ops = sum(float(o.nominal or 0) for o in q_ops_p.all() if o.dapur_id == d.id)
        d_pending = d_p_blj + d_p_ops

        # Burn rate harian (ambil belanja 14 hari terakhir)
        fourteen_days_ago = date.today() - timedelta(days=14)
        recent_total = sum(float(b.total_belanja or 0) for b in all_blj if b.dapur_id == d.id and b.tanggal >= fourteen_days_ago) + \
                       sum(float(o.nominal or 0) for o in all_ops if o.dapur_id == d.id and o.tanggal >= fourteen_days_ago)
        d_burn_rate = round(recent_total / 14.0, 0) if recent_total > 0 else (round(d_total_keluar / 7.0, 0) if d_total_keluar > 0 else 0)

        # Runway (hari)
        saldo_d = float(d.saldo_va or 0.0)
        if saldo_d <= 0:
            d_runway = 0.0
            health_status = 'KRITIS'
            health_class = 'danger'
            jumlah_kritis += 1
        elif d_burn_rate > 0:
            d_runway = round(saldo_d / d_burn_rate, 1)
            if d_runway < 3.0 or saldo_d < 3000000:
                health_status = 'WASPADA'
                health_class = 'warning'
                jumlah_waspada += 1
            else:
                health_status = 'AMAN'
                health_class = 'success'
        else:
            d_runway = 99.0
            health_status = 'AMAN'
            health_class = 'success'

        dapur_analysis.append({
            'id': d.id,
            'nama': d.nama,
            'kode': d.kode or d.nama[:4].upper(),
            'alamat': d.alamat or '',
            'bank_va': d.bank_va or 'BRI',
            'nomor_va': d.nomor_va or '',
            'saldo_va': saldo_d,
            'total_belanja': d_blj,
            'total_operasional': d_ops,
            'total_pengeluaran': d_total_keluar,
            'pending_pengeluaran': d_pending,
            'burn_rate_harian': d_burn_rate,
            'runway_hari': d_runway,
            'health_status': health_status,
            'health_class': health_class
        })

    # Burn Rate Keseluruhan
    total_burn_rate = sum(d['burn_rate_harian'] for d in dapur_analysis)
    total_runway = round(total_saldo_semua / total_burn_rate, 1) if (total_burn_rate > 0 and total_saldo_semua > 0) else 0.0

    # Tren Pengeluaran Harian (14 hari terakhir)
    hari_ini = date.today()
    tren_harian = []
    for i in range(13, -1, -1):
        dt = hari_ini - timedelta(days=i)
        dt_str = dt.strftime('%Y-%m-%d')
        label = dt.strftime('%d/%m')
        b_sum = sum(float(b.total_belanja or 0) for b in all_blj if b.tanggal == dt)
        o_sum = sum(float(o.nominal or 0) for o in all_ops if o.tanggal == dt)
        tren_harian.append({
            'tanggal': dt_str,
            'label': label,
            'belanja': b_sum,
            'operasional': o_sum,
            'total': b_sum + o_sum
        })

    # Distribusi Kategori Pengeluaran
    kategori_map = {}
    for b in all_blj:
        kat = b.kategori or 'Bahan Belanja'
        kategori_map[kat] = kategori_map.get(kat, 0.0) + float(b.total_belanja or 0)
    for o in all_ops:
        kat = f"Ops: {o.kategori}"
        kategori_map[kat] = kategori_map.get(kat, 0.0) + float(o.nominal or 0)

    distribusi_kategori = [
        {'kategori': k, 'total': v, 'persen': round((v / total_pengeluaran * 100), 1) if total_pengeluaran > 0 else 0}
        for k, v in sorted(kategori_map.items(), key=lambda x: x[1], reverse=True)
    ]

    return jsonify({
        'summary': {
            'total_saldo_va': total_saldo_semua,
            'total_belanja': total_belanja_valid,
            'total_operasional': total_ops_valid,
            'total_pengeluaran': total_pengeluaran,
            'total_pending': total_pending,
            'burn_rate_harian': total_burn_rate,
            'runway_hari': total_runway,
            'jumlah_kritis': jumlah_kritis,
            'jumlah_waspada': jumlah_waspada,
            'jumlah_dapur': len(dapurs)
        },
        'dapurs': dapur_analysis,
        'tren_harian': tren_harian,
        'distribusi_kategori': distribusi_kategori,
        'mutasi': [m.to_dict() for m in mutasi_list]
    })


# ─────────────────────────────────────────
# ROUTES – KATEGORI PENGAJUAN (CRUD)
# ─────────────────────────────────────────

@app.route('/api/kategori', methods=['GET'])
def api_kategori_list():
    jenis = request.args.get('jenis', '')
    only_active = request.args.get('aktif', '')
    q = KategoriPengajuan.query
    if jenis:
        q = q.filter_by(jenis=jenis.upper())
    if only_active in ['1', 'true', 'True']:
        q = q.filter_by(aktif=True)
    items = q.order_by(KategoriPengajuan.jenis.asc(), KategoriPengajuan.urutan.asc(), KategoriPengajuan.nama.asc()).all()
    return jsonify([k.to_dict() for k in items])

@app.route('/api/kategori', methods=['POST'])
def api_kategori_create():
    data = request.json or {}
    nama = data.get('nama', '').strip()
    if not nama:
        return jsonify({'error': 'Nama kategori wajib diisi'}), 400
    jenis = data.get('jenis', 'BARANG').strip().upper()
    kat = KategoriPengajuan(
        nama=nama,
        jenis=jenis,
        deskripsi=data.get('deskripsi', '').strip(),
        urutan=int(data.get('urutan') or 0),
        aktif=bool(data.get('aktif', True))
    )
    db.session.add(kat)
    db.session.commit()
    return jsonify(kat.to_dict()), 201

@app.route('/api/kategori/<int:kid>', methods=['PUT'])
def api_kategori_update(kid):
    kat = KategoriPengajuan.query.get_or_404(kid)
    data = request.json or {}
    if 'nama' in data:
        nama = data.get('nama', '').strip()
        if not nama:
            return jsonify({'error': 'Nama kategori tidak boleh kosong'}), 400
        kat.nama = nama
    if 'jenis' in data:
        kat.jenis = data.get('jenis', 'BARANG').strip().upper()
    if 'deskripsi' in data:
        kat.deskripsi = data.get('deskripsi', '').strip()
    if 'urutan' in data:
        kat.urutan = int(data.get('urutan') or 0)
    if 'aktif' in data:
        kat.aktif = bool(data.get('aktif'))
    db.session.commit()
    return jsonify(kat.to_dict())

@app.route('/api/kategori/<int:kid>', methods=['DELETE'])
def api_kategori_delete(kid):
    kat = KategoriPengajuan.query.get_or_404(kid)
    db.session.delete(kat)
    db.session.commit()
    return jsonify({'ok': True})


# ─────────────────────────────────────────
# ROUTES – OPERASIONAL
# ─────────────────────────────────────────

@app.route('/operasional')
def operasional():
    from sqlalchemy import or_
    pemasok_list = Pemasok.query.filter(
        Pemasok.aktif == True,
        or_(Pemasok.tipe == 'OPERASIONAL', Pemasok.tipe == 'SEMUA')
    ).order_by(Pemasok.nama).all()
    pemasok_json = [p.to_dict() for p in pemasok_list]
    dapur_list   = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    dapur_json   = [d.to_dict() for d in dapur_list]
    kategori_ops = get_kategori_list(jenis='OPERASIONAL')
    nomor_saran  = generate_nomor_operasional()
    return render_template('operasional.html',
                           pemasok_json=pemasok_json,
                           dapur_json=dapur_json,
                           kategori_ops=kategori_ops,
                           nomor_saran=nomor_saran,
                           today_iso=date.today().strftime('%Y-%m-%d'))

@app.route('/operasional.html')
@app.route('/templates/operasional.html')
def operasional_alias():
    return redirect(url_for('operasional'))

@app.route('/api/operasional', methods=['GET'])
def api_operasional_list():
    tgl_dari   = request.args.get('tgl_dari', '')
    tgl_sampai = request.args.get('tgl_sampai', '')
    dapur_id   = request.args.get('dapur_id', '')
    kategori   = request.args.get('kategori', '')
    status     = request.args.get('status', '')

    q = PengajuanOperasional.query
    if tgl_dari:
        q = q.filter(PengajuanOperasional.tanggal >= datetime.strptime(tgl_dari, '%Y-%m-%d').date())
    if tgl_sampai:
        q = q.filter(PengajuanOperasional.tanggal <= datetime.strptime(tgl_sampai, '%Y-%m-%d').date())
    if dapur_id:
        if dapur_id in ['none', '0']:
            q = q.filter(PengajuanOperasional.dapur_id == None)
        else:
            q = q.filter(PengajuanOperasional.dapur_id == int(dapur_id))
    if kategori:
        q = q.filter(PengajuanOperasional.kategori == kategori)
    if status:
        q = q.filter(PengajuanOperasional.status == status)

    data = q.order_by(PengajuanOperasional.tanggal.desc(), PengajuanOperasional.created_at.desc()).all()
    return jsonify([d.to_dict() for d in data])

@app.route('/api/operasional', methods=['POST'])
def api_operasional_create():
    data = request.json or {}
    nominal = float(data.get('nominal', 0) or 0)
    bank = data.get('bank_penerima', '').strip()
    biaya, transfer = hitung_admin_fee(bank, nominal)

    nomor = data.get('nomor', '').strip() or generate_nomor_operasional()
    did = data.get('dapur_id')
    pid = data.get('pemasok_id')

    ops = PengajuanOperasional(
        nomor=nomor,
        dapur_id=int(did) if did else None,
        pemasok_id=int(pid) if pid else None,
        nama_penerima=data.get('nama_penerima', '').strip(),
        bank_penerima=bank,
        no_rekening_penerima=data.get('no_rekening_penerima', '').strip(),
        tanggal=datetime.strptime(data.get('tanggal', date.today().strftime('%Y-%m-%d')), '%Y-%m-%d').date(),
        kategori=data.get('kategori', '').strip(),
        keperluan=data.get('keperluan', '').strip(),
        nominal=nominal,
        biaya_admin=biaya,
        total_transfer=transfer,
        status=data.get('status', 'diajukan'),
        keterangan=data.get('keterangan', '').strip()
    )
    db.session.add(ops)
    db.session.commit()

    # Log Audit CREATE
    log_audit(
        aksi='CREATE',
        modul='OPERASIONAL',
        ref_id=ops.id,
        deskripsi=f"Input operasional [{ops.nomor}] {ops.kategori} - {ops.keperluan} (Rp {ops.nominal:,.0f})",
        data_baru=ops.to_dict()
    )

    # Trigger Notifikasi Pengajuan Operasional Baru
    if get_setting('notif_event_pengajuan', '1') == '1':
        dapur_nama = ops.dapur.nama if ops.dapur else get_setting('nama_dapur', 'SPPG')
        kirim_notifikasi_async(
            "🔔 PENGAJUAN BIAYA OPERASIONAL BARU",
            [
                ("No. Pengajuan", ops.nomor),
                ("Unit Dapur", dapur_nama),
                ("Tanggal", ops.tanggal.strftime('%d/%m/%Y')),
                ("Kategori", ops.kategori),
                ("Keperluan", ops.keperluan),
                ("Nominal", f"Rp {ops.nominal:,.0f}"),
                ("Net Transfer", f"Rp {ops.total_transfer:,.0f} ({ops.bank_penerima} - {ops.no_rekening_penerima})"),
                ("Penerima", ops.nama_penerima),
            ],
            "Menunggu verifikasi dan validasi bendahara.",
            dapur_id=ops.dapur_id,
            event_type='pengajuan'
        )

    return jsonify(ops.to_dict()), 201

@app.route('/api/operasional/<int:oid>', methods=['PUT'])
def api_operasional_update(oid):
    ops = PengajuanOperasional.query.get_or_404(oid)
    data_lama_snapshot = ops.to_dict()
    data = request.json or {}
    if 'dapur_id' in data:
        ops.dapur_id = int(data['dapur_id']) if data['dapur_id'] else None
    if 'pemasok_id' in data:
        ops.pemasok_id = int(data['pemasok_id']) if data['pemasok_id'] else None
    if 'nama_penerima' in data:
        ops.nama_penerima = data['nama_penerima'].strip()
    if 'bank_penerima' in data:
        ops.bank_penerima = data['bank_penerima'].strip()
    if 'no_rekening_penerima' in data:
        ops.no_rekening_penerima = data['no_rekening_penerima'].strip()
    if 'tanggal' in data:
        ops.tanggal = datetime.strptime(data['tanggal'], '%Y-%m-%d').date()
    if 'kategori' in data:
        ops.kategori = data['kategori'].strip()
    if 'keperluan' in data:
        ops.keperluan = data['keperluan'].strip()
    if 'nominal' in data:
        ops.nominal = float(data['nominal'] or 0)
    if 'keterangan' in data:
        ops.keterangan = data['keterangan'].strip()
    if 'status' in data:
        ops.status = data['status']

    # Hitung ulang admin fee
    ops.biaya_admin, ops.total_transfer = hitung_admin_fee(ops.bank_penerima, ops.nominal)
    db.session.commit()

    # Log Audit UPDATE
    log_audit(
        aksi='UPDATE',
        modul='OPERASIONAL',
        ref_id=ops.id,
        deskripsi=f"Perbarui operasional [{ops.nomor}] {ops.kategori} - {ops.keperluan} (Rp {ops.nominal:,.0f})",
        data_lama=data_lama_snapshot,
        data_baru=ops.to_dict()
    )

    return jsonify(ops.to_dict())

@app.route('/api/operasional/<int:oid>', methods=['DELETE'])
def api_operasional_delete(oid):
    ops = PengajuanOperasional.query.get_or_404(oid)
    data_lama_snapshot = ops.to_dict()
    nomor_ops = ops.nomor
    ref_id = ops.id
    db.session.delete(ops)
    db.session.commit()

    # Log Audit DELETE with can_restore=True for Undo / Pulihkan Data
    log_audit(
        aksi='DELETE',
        modul='OPERASIONAL',
        ref_id=ref_id,
        deskripsi=f"Hapus operasional [{nomor_ops}] {data_lama_snapshot.get('kategori','')} - {data_lama_snapshot.get('keperluan','')} (Rp {data_lama_snapshot.get('nominal',0):,.0f})",
        data_lama=data_lama_snapshot,
        can_restore=True
    )

    return jsonify({'ok': True})

@app.route('/api/operasional/<int:oid>/status', methods=['POST'])
def api_operasional_toggle_status(oid):
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Hanya Validator atau Super Admin yang berhak memvalidasi operasional.'}), 403
    ops = PengajuanOperasional.query.get_or_404(oid)
    data = request.json or {}
    prev_status = ops.status
    new_status = data.get('status')
    if new_status:
        ops.status = new_status
    else:
        ops.status = 'divalidasi' if ops.status != 'divalidasi' else 'diajukan'

    # Potong / Kembalikan Saldo VA Dapur terkait
    # Pengurangan saldo VA adalah Total Nominal Operasional (biaya admin non-BRI dipotong dari nominal dan dibebankan ke saldo VA)
    nominal_potong = float(ops.nominal or 0.0)
    dapur_terkait = Dapur.query.get(ops.dapur_id) if ops.dapur_id else None

    if ops.status == 'divalidasi':
        validator_name = (curr.nama_lengkap if curr else None) or data.get('validator', 'Validator')
        ops.validated_by = validator_name
        ops.validated_at = datetime.utcnow()
        # Jika disetujui, saldo VA dapur berkurang otomatis & catat mutasi keluar
        if dapur_terkait:
            s_seb = float(dapur_terkait.saldo_va or 0.0)
            dapur_terkait.saldo_va = s_seb - nominal_potong
            
            ket_mutasi = f"Operasional [{ops.nomor}] {ops.kategori}: {ops.keperluan}"
            if ops.biaya_admin and ops.biaya_admin > 0:
                ket_mutasi += f" (Trf Rp {ops.total_transfer:,.0f} + Adm Rp {ops.biaya_admin:,.0f})"
            
            mutasi_ops = MutasiVA(
                dapur_id=dapur_terkait.id,
                tanggal=ops.tanggal or date.today(),
                waktu=datetime.utcnow(),
                tipe='KELUAR',
                kategori='OPERASIONAL',
                nominal=nominal_potong,
                saldo_sebelum=s_seb,
                saldo_sesudah=dapur_terkait.saldo_va,
                keterangan=ket_mutasi,
                ref_nomor=f"{ops.nomor}",
                operator=validator_name
            )
            db.session.add(mutasi_ops)
    else:
        ops.validated_by = ''
        ops.validated_at = None
        # Jika validasi dibatalkan, kembalikan saldo VA dapur & catat mutasi masuk
        if dapur_terkait:
            s_seb = float(dapur_terkait.saldo_va or 0.0)
            dapur_terkait.saldo_va = s_seb + nominal_potong
            mutasi_cancel = MutasiVA(
                dapur_id=dapur_terkait.id,
                tanggal=date.today(),
                waktu=datetime.utcnow(),
                tipe='MASUK',
                kategori='PENYESUAIAN',
                nominal=nominal_potong,
                saldo_sebelum=s_seb,
                saldo_sesudah=dapur_terkait.saldo_va,
                keterangan=f"Batal Validasi Operasional [{ops.nomor}] (Rp {ops.nominal:,.0f})",
                ref_nomor=f"{ops.nomor}-CANCEL",
                operator=curr.nama_lengkap if curr else 'System'
            )
            db.session.add(mutasi_cancel)

    db.session.commit()

    # Log Audit VALIDATE
    log_audit(
        aksi='VALIDATE' if ops.status == 'divalidasi' else 'UNVALIDATE',
        modul='OPERASIONAL',
        ref_id=ops.id,
        deskripsi=f"{'Validasi Disetujui (Potong Saldo VA Rp ' + f'{nominal_potong:,.0f}' + ')' if ops.status == 'divalidasi' else 'Batal Validasi (Kembalikan Saldo VA)'} Operasional [{ops.nomor}] {ops.kategori} (Rp {ops.nominal:,.0f})",
        data_lama={'status': prev_status},
        data_baru={'status': ops.status, 'validated_by': ops.validated_by, 'sisa_saldo_va': dapur_terkait.saldo_va if dapur_terkait else None}
    )

    # Trigger Notifikasi Validasi Operasional Disetujui
    if ops.status == 'divalidasi' and get_setting('notif_event_validasi', '1') == '1':
        dapur_nama = ops.dapur.nama if ops.dapur else get_setting('nama_dapur', 'SPPG')
        kirim_notifikasi_async(
            "✅ VALIDASI BIAYA OPERASIONAL DISETUJUI",
            [
                ("No. Pengajuan", ops.nomor),
                ("Unit Dapur", dapur_nama),
                ("Kategori", ops.kategori),
                ("Keperluan", ops.keperluan),
                ("Total Biaya (Potong VA)", f"Rp {ops.nominal:,.0f}"),
                ("Potongan Admin", f"Rp {ops.biaya_admin:,.0f}" if ops.biaya_admin else "Rp 0 (BRI)"),
                ("Net Transfer", f"Rp {ops.total_transfer:,.0f} ({ops.bank_penerima} - {ops.no_rekening_penerima})"),
                ("Penerima", ops.nama_penerima),
                ("Divalidasi Oleh", ops.validated_by or 'Validator'),
                ("Waktu Validasi", datetime.now().strftime('%d/%m/%Y %H:%M WIB')),
            ],
            "Pengajuan telah divalidasi dan siap diproses transfer bank.",
            dapur_id=ops.dapur_id,
            event_type='validasi'
        )

    return jsonify(ops.to_dict())



# ─────────────────────────────────────────
# ROUTES - PENGELUARAN WAJIB & OPSIONAL (NON-VA)
# ─────────────────────────────────────────


def init_pengeluaran_wajib_db():
    """Inisialisasi tabel Pengeluaran Wajib & Kategorinya jika belum ada (aman untuk cPanel SQLite)."""
    try:
        with db.engine.connect() as conn:
            conn.execute(db.text("""
                CREATE TABLE IF NOT EXISTS kategori_pengeluaran_wajib (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    nama VARCHAR(100) NOT NULL UNIQUE,
                    sifat VARCHAR(20) DEFAULT 'WAJIB',
                    deskripsi VARCHAR(255) DEFAULT '',
                    urutan INTEGER DEFAULT 0,
                    aktif BOOLEAN DEFAULT 1,
                    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                );
            """))
            conn.execute(db.text("""
                CREATE TABLE IF NOT EXISTS pengeluaran_wajib (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    nomor VARCHAR(50) NOT NULL UNIQUE,
                    dapur_id INTEGER,
                    kategori_id INTEGER,
                    kategori_nama VARCHAR(100) DEFAULT '',
                    sifat VARCHAR(20) DEFAULT 'WAJIB',
                    tanggal DATE NOT NULL,
                    judul VARCHAR(200) NOT NULL,
                    nama_penerima VARCHAR(150) DEFAULT '',
                    bank_penerima VARCHAR(50) DEFAULT '',
                    no_rekening_penerima VARCHAR(50) DEFAULT '',
                    metode_bayar VARCHAR(30) DEFAULT 'TRANSFER',
                    nominal FLOAT DEFAULT 0.0,
                    biaya_admin FLOAT DEFAULT 0.0,
                    total_bayar FLOAT DEFAULT 0.0,
                    keperluan TEXT DEFAULT '',
                    status VARCHAR(30) DEFAULT 'draft',
                    disetujui_oleh VARCHAR(100),
                    disetujui_pada DATETIME,
                    created_by VARCHAR(100) DEFAULT 'Admin',
                    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
                );
            """))
            conn.commit()

            # Seed kategori jika masih kosong
            row = conn.execute(db.text("SELECT COUNT(*) FROM kategori_pengeluaran_wajib")).fetchone()
            if row and row[0] == 0:
                default_pw_kat = [
                    ('Tagihan', 'WAJIB', 'Tagihan rutin bulanan (listrik PLN, air PDAM, WiFi, sewa gedung, langganan)', 1),
                    ('Operasional (Optional)', 'OPSIONAL', 'Biaya operasional tambahan non-VA', 2),
                    ('Tukang', 'OPSIONAL', 'Biaya tukang harian/borongan bangunan atau instalasi', 3),
                    ('Pemeliharaan Gedung', 'OPSIONAL', 'Perawatan bangunan, renovasi kecil, perbaikan atap, cat, dll', 4),
                    ('Pemeliharaan Alat', 'OPSIONAL', 'Servis kompor, reparasi chiller/freezer, blender, instalasi gas', 5),
                    ('Lain-lain', 'OPSIONAL', 'Pengeluaran insidental lainnya di luar kategori utama', 6),
                    ('Tagihan Suplayer Bahan', 'SUPPLIER', 'Pembayaran bahan pangan dan belanja suplayer', 1),
                    ('Penggajian Karyawan', 'PENGGAJIAN', 'Gaji karyawan tetap bulanan', 1),
                    ('Honor / Honorarium', 'PENGGAJIAN', 'Honor staf harian, tenaga lepas, dan jasa', 2),
                    ('Tunjangan', 'PENGGAJIAN', 'Tunjangan makan, transport, atau kinerja', 3),
                    ('Upah Harian', 'PENGGAJIAN', 'Upah tenaga kerja harian/mingguan', 4),
                ]
                for nama, sifat, desc, urut in default_pw_kat:
                    conn.execute(db.text("""
                        INSERT OR IGNORE INTO kategori_pengeluaran_wajib (nama, sifat, deskripsi, urutan, aktif)
                        VALUES (:nama, :sifat, :desc, :urut, 1)
                    """), {'nama': nama, 'sifat': sifat, 'desc': desc, 'urut': urut})
                conn.commit()
    except Exception as e:
        print(f"[INIT PW DB ERROR] {e}")

def generate_nomor_pengeluaran_wajib():
    """Menghasilkan nomor unik pengeluaran format PW-YYYYMM-XXXX"""
    now = datetime.now()
    prefix = f"PW-{now.strftime('%Y%m')}-"
    last = PengeluaranWajib.query.filter(
        PengeluaranWajib.nomor.like(f"{prefix}%")
    ).order_by(PengeluaranWajib.id.desc()).first()
    if last:
        try:
            seq = int(last.nomor.split('-')[-1]) + 1
        except Exception:
            seq = 1
    else:
        seq = 1
    return f"{prefix}{seq:04d}"


@app.route('/pengeluaran-wajib')
@login_required
def pengeluaran_wajib():
    try:
        if not check_permission('pengeluaran_wajib'):
            flash('Anda tidak memiliki hak akses ke halaman Pengeluaran Wajib & Opsional.', 'danger')
            return redirect(url_for('index'))

        # Pastikan tabel pengeluaran wajib & kategorinya sudah ada jika belum terbuat di hosting
        init_pengeluaran_wajib_db()

        dapur_list = []
        try:
            dapur_list = Dapur.query.filter_by(aktif=True).order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
            dapur_json = [d.to_dict() for d in dapur_list]
        except Exception as e:
            print(f'[PW DAPUR ERROR] {e}')
            dapur_json = []

        kategori_json = []
        try:
            kategori_list = KategoriPengeluaranWajib.query.filter_by(aktif=True).order_by(
                KategoriPengeluaranWajib.sifat.desc(),
                KategoriPengeluaranWajib.urutan.asc(),
                KategoriPengeluaranWajib.nama.asc()
            ).all()
            kategori_json = [k.to_dict() for k in kategori_list]
        except Exception as e:
            print(f'[PW KATEGORI ERROR] {e}')
            kategori_json = []

        try:
            nomor_saran = generate_nomor_pengeluaran_wajib()
        except Exception:
            nomor_saran = 'PW-0001'
        today_iso = date.today().strftime('%Y-%m-%d')

        return render_template('pengeluaran_wajib.html',
                               dapur_json=dapur_json,
                               kategori_json=kategori_json,
                               nomor_saran=nomor_saran,
                               today_iso=today_iso)
    except Exception as e:
        import traceback
        err_detail = traceback.format_exc()
        print(f"[ERROR /pengeluaran-wajib] {err_detail}")
        try:
            log_path = os.path.join(BASE_DIR, 'error_debug.log')
            with open(log_path, 'a', encoding='utf-8') as f_log:
                f_log.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] /pengeluaran-wajib ERROR:\n{err_detail}\n")
        except Exception:
            pass
        return f"""
        <div style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;padding:30px;max-width:800px;margin:30px auto;border:1px solid #fca5a5;border-radius:12px;background:#fef2f2;box-shadow:0 4px 12px rgba(0,0,0,0.05);">
            <h3 style="color:#b91c1c;margin-top:0;">Terjadi Kesalahan saat Membuka Pengeluaran Wajib</h3>
            <p style="color:#475569;"><strong>Keterangan:</strong> {e}</p>
            <pre style="background:#1e293b;color:#f8fafc;padding:14px;border-radius:8px;font-size:12px;overflow-x:auto;">{err_detail}</pre>
            <p><a href="/" style="background:#2563eb;color:#fff;padding:8px 16px;text-decoration:none;border-radius:6px;font-weight:bold;">Kembali ke Beranda</a></p>
        </div>
        """, 500

@app.route('/pengeluaran_wajib.html')
@app.route('/templates/pengeluaran_wajib.html')
def pengeluaran_wajib_alias():
    return redirect(url_for('pengeluaran_wajib'))


@app.route('/api/pengeluaran-wajib', methods=['GET'])
@login_required
def api_pengeluaran_wajib_list():
    if not check_permission('pengeluaran_wajib'):
        return jsonify({'error': 'Akses ditolak'}), 403

    init_pengeluaran_wajib_db()
    try:
        q_query = PengeluaranWajib.query
    except Exception as e:
        return jsonify({'items': [], 'stats': {}, 'nomor_saran': 'PW-0001', 'error': str(e)})

    # Filter dapur jika ada
    dapur_id = request.args.get('dapur_id', '')
    if dapur_id and dapur_id != 'all':
        try:
            q_query = q_query.filter(PengeluaranWajib.dapur_id == int(dapur_id))
        except ValueError:
            pass

    # Filter sifat (WAJIB / OPSIONAL / PENGGAJIAN / SUPPLIER)
    sifat = request.args.get('sifat', '').upper()
    if sifat in ['WAJIB', 'OPSIONAL', 'PENGGAJIAN', 'SUPPLIER']:
        q_query = q_query.filter(PengeluaranWajib.sifat == sifat)

    # Filter kategori_id
    kategori_id = request.args.get('kategori_id', '')
    if kategori_id and kategori_id != 'all':
        try:
            q_query = q_query.filter(PengeluaranWajib.kategori_id == int(kategori_id))
        except ValueError:
            pass

    # Filter status (draft / disetujui)
    status = request.args.get('status', '').lower()
    if status in ['draft', 'disetujui']:
        q_query = q_query.filter(PengeluaranWajib.status == status)

    # Filter bulan & tahun
    bulan = request.args.get('bulan', '')
    tahun = request.args.get('tahun', '')
    if bulan and bulan != 'all':
        try:
            q_query = q_query.filter(db.func.strftime('%m', PengeluaranWajib.tanggal) == f"{int(bulan):02d}")
        except Exception:
            pass
    if tahun and tahun != 'all':
        try:
            q_query = q_query.filter(db.func.strftime('%Y', PengeluaranWajib.tanggal) == str(tahun))
        except Exception:
            pass

    # Search keyword
    keyword = request.args.get('q', '').strip()
    if keyword:
        kw = f"%{keyword}%"
        q_query = q_query.filter(
            db.or_(
                PengeluaranWajib.judul.ilike(kw),
                PengeluaranWajib.nama_penerima.ilike(kw),
                PengeluaranWajib.keperluan.ilike(kw),
                PengeluaranWajib.nomor.ilike(kw),
                PengeluaranWajib.kategori_nama.ilike(kw)
            )
        )

    items = q_query.order_by(PengeluaranWajib.tanggal.desc(), PengeluaranWajib.id.desc()).all()
    items_json = [i.to_dict() for i in items]

    total_wajib = sum(float(i.total_bayar or 0.0) for i in items if (i.sifat or '').upper() == 'WAJIB')
    total_opsional = sum(float(i.total_bayar or 0.0) for i in items if (i.sifat or '').upper() == 'OPSIONAL')
    grand_total = sum(float(i.total_bayar or 0.0) for i in items)
    count_disetujui = sum(1 for i in items if i.status == 'disetujui')
    count_draft = len(items) - count_disetujui

    stats = {
        'total_wajib': total_wajib,
        'total_opsional': total_opsional,
        'grand_total': grand_total,
        'count_total': len(items),
        'count_disetujui': count_disetujui,
        'count_draft': count_draft
    }

    return jsonify({'items': items_json, 'stats': stats, 'nomor_saran': generate_nomor_pengeluaran_wajib()})


@app.route('/api/pengeluaran-wajib', methods=['POST'])
@login_required
def api_pengeluaran_wajib_create():
    if not check_permission('fitur_pengeluaran_wajib_tambah') and not check_permission('pengeluaran_wajib'):
        return jsonify({'error': 'Anda tidak memiliki hak akses menambah pengeluaran wajib/opsional'}), 403

    data = request.json or {}
    judul = data.get('judul', '').strip()
    if not judul:
        return jsonify({'error': 'Nama/Judul Pengeluaran wajib diisi'}), 400

    tanggal_str = data.get('tanggal', '')
    try:
        tgl = datetime.strptime(tanggal_str, '%Y-%m-%d').date() if tanggal_str else date.today()
    except Exception:
        tgl = date.today()

    nomor = data.get('nomor', '').strip() or generate_nomor_pengeluaran_wajib()
    if PengeluaranWajib.query.filter_by(nomor=nomor).first():
        nomor = generate_nomor_pengeluaran_wajib()

    dapur_id = data.get('dapur_id')
    dapur_id = int(dapur_id) if (dapur_id and str(dapur_id) != 'null') else None

    kategori_id = data.get('kategori_id')
    kategori_id = int(kategori_id) if (kategori_id and str(kategori_id) != 'null') else None
    kategori_obj = KategoriPengeluaranWajib.query.get(kategori_id) if kategori_id else None
    kategori_nama = kategori_obj.nama if kategori_obj else (data.get('kategori_nama', '').strip() or 'Lain-lain')
    sifat = (kategori_obj.sifat if kategori_obj else data.get('sifat', 'WAJIB')).upper()

    nama_penerima = data.get('nama_penerima', '').strip()
    bank_penerima = data.get('bank_penerima', '').strip()
    no_rekening = data.get('no_rekening_penerima', '').strip()
    metode_bayar = (data.get('metode_bayar', 'TRANSFER') or 'TRANSFER').upper()

    nominal = float(data.get('nominal') or 0.0)
    biaya_admin = float(data.get('biaya_admin') or 0.0)
    total_bayar = nominal + biaya_admin

    curr = get_current_user()
    pw = PengeluaranWajib(
        nomor=nomor,
        dapur_id=dapur_id,
        kategori_id=kategori_id,
        kategori_nama=kategori_nama,
        sifat=sifat,
        tanggal=tgl,
        judul=judul,
        nama_penerima=nama_penerima,
        bank_penerima=bank_penerima,
        no_rekening_penerima=no_rekening,
        metode_bayar=metode_bayar,
        nominal=nominal,
        biaya_admin=biaya_admin,
        total_bayar=total_bayar,
        keperluan=data.get('keperluan', '').strip(),
        status='draft',
        created_by=curr.nama_lengkap if curr else 'Admin'
    )
    db.session.add(pw)
    db.session.commit()

    log_audit(
        aksi='CREATE',
        modul='PENGELUARAN_WAJIB',
        ref_id=pw.id,
        deskripsi=f"Tambah pengeluaran {sifat} [{pw.nomor}] {pw.judul} (Rp {pw.total_bayar:,.0f})",
        data_baru=pw.to_dict()
    )

    return jsonify(pw.to_dict()), 201


@app.route('/api/pengeluaran-wajib/<int:pid>', methods=['PUT'])
@login_required
def api_pengeluaran_wajib_update(pid):
    pw = PengeluaranWajib.query.get_or_404(pid)
    data_lama = pw.to_dict()
    data = request.json or {}

    if 'judul' in data:
        pw.judul = data['judul'].strip()
    if 'tanggal' in data and data['tanggal']:
        try:
            pw.tanggal = datetime.strptime(data['tanggal'], '%Y-%m-%d').date()
        except Exception:
            pass
    if 'dapur_id' in data:
        pw.dapur_id = int(data['dapur_id']) if (data['dapur_id'] and str(data['dapur_id']) != 'null') else None
    if 'kategori_id' in data:
        pw.kategori_id = int(data['kategori_id']) if (data['kategori_id'] and str(data['kategori_id']) != 'null') else None
        k_obj = KategoriPengeluaranWajib.query.get(pw.kategori_id) if pw.kategori_id else None
        if k_obj:
            pw.kategori_nama = k_obj.nama
            pw.sifat = k_obj.sifat
    if 'sifat' in data and data['sifat']:
        pw.sifat = data['sifat'].upper()
    if 'nama_penerima' in data:
        pw.nama_penerima = data['nama_penerima'].strip()
    if 'bank_penerima' in data:
        pw.bank_penerima = data['bank_penerima'].strip()
    if 'no_rekening_penerima' in data:
        pw.no_rekening_penerima = data['no_rekening_penerima'].strip()
    if 'metode_bayar' in data:
        pw.metode_bayar = (data['metode_bayar'] or 'TRANSFER').upper()
    if 'nominal' in data:
        pw.nominal = float(data['nominal'] or 0.0)
    if 'biaya_admin' in data:
        pw.biaya_admin = float(data['biaya_admin'] or 0.0)
    pw.total_bayar = float(pw.nominal or 0.0) + float(pw.biaya_admin or 0.0)

    if 'keperluan' in data:
        pw.keperluan = data['keperluan'].strip()

    db.session.commit()

    log_audit(
        aksi='UPDATE',
        modul='PENGELUARAN_WAJIB',
        ref_id=pw.id,
        deskripsi=f"Perbarui pengeluaran {pw.sifat} [{pw.nomor}] {pw.judul} (Rp {pw.total_bayar:,.0f})",
        data_lama=data_lama,
        data_baru=pw.to_dict()
    )

    return jsonify(pw.to_dict())


@app.route('/api/pengeluaran-wajib/<int:pid>', methods=['DELETE'])
@login_required
def api_pengeluaran_wajib_delete(pid):
    curr = get_current_user()
    if not check_permission('fitur_pengeluaran_wajib_hapus') and (not curr or curr.role != 'super_admin'):
        return jsonify({'error': 'Anda tidak memiliki izin menghapus pengeluaran wajib/opsional'}), 403

    pw = PengeluaranWajib.query.get_or_404(pid)
    data_lama = pw.to_dict()
    nomor = pw.nomor
    ref_id = pw.id
    db.session.delete(pw)
    db.session.commit()

    log_audit(
        aksi='DELETE',
        modul='PENGELUARAN_WAJIB',
        ref_id=ref_id,
        deskripsi=f"Hapus pengeluaran [{nomor}] {data_lama.get('judul','')} (Rp {data_lama.get('total_bayar',0):,.0f})",
        data_lama=data_lama,
        can_restore=True
    )

    return jsonify({'ok': True})


@app.route('/api/pengeluaran-wajib/<int:pid>/duplikat', methods=['POST'])
@login_required
def api_pengeluaran_wajib_duplikat(pid):
    if not check_permission('fitur_pengeluaran_wajib_tambah') and not check_permission('pengeluaran_wajib'):
        return jsonify({'error': 'Anda tidak memiliki hak akses menambah/duplikat pengeluaran'}), 403

    sumber = PengeluaranWajib.query.get_or_404(pid)
    data = request.get_json(silent=True) or {}

    tanggal_str = data.get('tanggal')
    if tanggal_str:
        try:
            tgl = datetime.strptime(tanggal_str, '%Y-%m-%d').date()
        except Exception:
            tgl = date.today()
    else:
        # Default: 1 bulan setelah tanggal sumber
        tgl_lama = sumber.tanggal or date.today()
        # Hitung bulan berikutnya dengan aman
        thn = tgl_lama.year
        bln = tgl_lama.month + 1
        if bln > 12:
            bln = 1
            thn += 1
        # Ambil tanggal yang valid untuk bulan target
        hari = min(tgl_lama.day, 28)
        tgl = date(thn, bln, hari)

    curr = get_current_user()
    nomor_baru = generate_nomor_pengeluaran_wajib()

    pw_baru = PengeluaranWajib(
        nomor=nomor_baru,
        dapur_id=sumber.dapur_id,
        kategori_id=sumber.kategori_id,
        kategori_nama=sumber.kategori_nama,
        sifat=sumber.sifat,
        tanggal=tgl,
        judul=data.get('judul', sumber.judul).strip(),
        nama_penerima=sumber.nama_penerima,
        bank_penerima=sumber.bank_penerima,
        no_rekening_penerima=sumber.no_rekening_penerima,
        metode_bayar=sumber.metode_bayar,
        nominal=float(data.get('nominal', sumber.nominal) or 0.0),
        biaya_admin=float(data.get('biaya_admin', sumber.biaya_admin) or 0.0),
        total_bayar=float(data.get('nominal', sumber.nominal) or 0.0) + float(data.get('biaya_admin', sumber.biaya_admin) or 0.0),
        keperluan=data.get('keperluan', sumber.keperluan or '').strip(),
        status='draft',
        created_by=curr.nama_lengkap if curr else 'Admin'
    )
    db.session.add(pw_baru)
    db.session.commit()

    log_audit(
        aksi='DUPLICATE',
        modul='PENGELUARAN_WAJIB',
        ref_id=pw_baru.id,
        deskripsi=f"Duplikat pengeluaran [{sumber.nomor}] ke [{pw_baru.nomor}] {pw_baru.judul} periode {pw_baru.tanggal.strftime('%b %Y')} (Rp {pw_baru.total_bayar:,.0f})",
        data_baru=pw_baru.to_dict()
    )

    return jsonify(pw_baru.to_dict()), 201


@app.route('/api/pengeluaran-wajib/<int:pid>/validasi', methods=['POST'])
@login_required
def api_pengeluaran_wajib_toggle_validasi(pid):
    curr = get_current_user()
    if not check_permission('fitur_pengeluaran_wajib_validasi') and (not curr or curr.role not in ['super_admin', 'validator']):
        return jsonify({'error': 'Hanya Validator atau Super Admin yang berhak menyetujui pengeluaran wajib.'}), 403

    pw = PengeluaranWajib.query.get_or_404(pid)
    data = request.get_json(silent=True) or {}
    action = data.get('action')

    if action == 'approve' or (not action and pw.status != 'disetujui'):
        pw.status = 'disetujui'
        pw.disetujui_oleh = curr.nama_lengkap if curr else 'Validator'
        pw.disetujui_pada = datetime.utcnow()
    else:
        pw.status = 'draft'
        pw.disetujui_oleh = None
        pw.disetujui_pada = None

    # Pengeluaran Wajib & Opsional NON-VA, TIDAK ADA pemotongan saldo VA!
    db.session.commit()

    log_audit(
        aksi='VALIDATE' if pw.status == 'disetujui' else 'UNVALIDATE',
        modul='PENGELUARAN_WAJIB',
        ref_id=pw.id,
        deskripsi=f"{'Persetujuan' if pw.status == 'disetujui' else 'Pembatalan'} pengeluaran [{pw.nomor}] {pw.judul} (Rp {pw.total_bayar:,.0f})",
        data_baru=pw.to_dict()
    )

    return jsonify(pw.to_dict())


# ── CRUD Kategori Pengeluaran Wajib ──
@app.route('/api/kategori-pengeluaran-wajib', methods=['GET'])
@login_required
def api_kategori_pengeluaran_wajib_list():
    init_pengeluaran_wajib_db()
    sifat = request.args.get('sifat', '').upper()
    try:
        q = KategoriPengeluaranWajib.query
    except Exception:
        return jsonify([])
    if sifat in ['WAJIB', 'OPSIONAL', 'PENGGAJIAN', 'SUPPLIER']:
        q = q.filter_by(sifat=sifat)
    items = q.order_by(
        KategoriPengeluaranWajib.sifat.desc(),
        KategoriPengeluaranWajib.urutan.asc(),
        KategoriPengeluaranWajib.nama.asc()
    ).all()
    return jsonify([i.to_dict() for i in items])


@app.route('/api/kategori-pengeluaran-wajib', methods=['POST'])
@login_required
def api_kategori_pengeluaran_wajib_create():
    if not check_permission('fitur_pengeluaran_wajib_kategori') and not check_permission('pengeluaran_wajib'):
        return jsonify({'error': 'Tidak memiliki hak akses menambah kategori pengeluaran'}), 403

    data = request.json or {}
    nama = data.get('nama', '').strip()
    if not nama:
        return jsonify({'error': 'Nama kategori wajib diisi'}), 400

    sifat = (data.get('sifat') or 'OPSIONAL').upper()
    if sifat not in ['WAJIB', 'OPSIONAL', 'PENGGAJIAN', 'SUPPLIER']:
        sifat = 'OPSIONAL'

    if KategoriPengeluaranWajib.query.filter_by(nama=nama).first():
        return jsonify({'error': f'Kategori "{nama}" sudah ada.'}), 400

    kat = KategoriPengeluaranWajib(
        nama=nama,
        sifat=sifat,
        deskripsi=data.get('deskripsi', '').strip(),
        urutan=int(data.get('urutan') or 0),
        aktif=bool(data.get('aktif', True))
    )
    db.session.add(kat)
    db.session.commit()
    return jsonify(kat.to_dict()), 201


@app.route('/api/kategori-pengeluaran-wajib/<int:kid>', methods=['PUT'])
@login_required
def api_kategori_pengeluaran_wajib_update(kid):
    kat = KategoriPengeluaranWajib.query.get_or_404(kid)
    data = request.json or {}
    if 'nama' in data:
        nama = data['nama'].strip()
        if not nama:
            return jsonify({'error': 'Nama kategori tidak boleh kosong'}), 400
        ada = KategoriPengeluaranWajib.query.filter(KategoriPengeluaranWajib.nama == nama, KategoriPengeluaranWajib.id != kid).first()
        if ada:
            return jsonify({'error': f'Kategori "{nama}" sudah digunakan.'}), 400
        kat.nama = nama
    if 'sifat' in data:
        kat.sifat = (data['sifat'] or 'OPSIONAL').upper()
    if 'deskripsi' in data:
        kat.deskripsi = data['deskripsi'].strip()
    if 'urutan' in data:
        kat.urutan = int(data['urutan'] or 0)
    if 'aktif' in data:
        kat.aktif = bool(data['aktif'])

    db.session.commit()
    return jsonify(kat.to_dict())


@app.route('/api/kategori-pengeluaran-wajib/<int:kid>', methods=['DELETE'])
@login_required
def api_kategori_pengeluaran_wajib_delete(kid):
    kat = KategoriPengeluaranWajib.query.get_or_404(kid)
    dipakai = PengeluaranWajib.query.filter_by(kategori_id=kid).count()
    if dipakai > 0:
        return jsonify({'error': f'Kategori "{kat.nama}" tidak dapat dihapus karena telah digunakan pada {dipakai} transaksi pengeluaran. Anda dapat menonaktifkannya.'}), 400

    db.session.delete(kat)
    db.session.commit()
    return jsonify({'ok': True})



# ─────────────────────────────────────────
# EXPORT – EXCEL
# ─────────────────────────────────────────

@app.route('/export/excel/<int:pid>')
def export_excel(pid):
    from openpyxl import Workbook
    from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
    from openpyxl.utils import get_column_letter

    pengajuan = PengajuanHeader.query.get_or_404(pid)
    wb = Workbook()
    ws = wb.active
    ws.title = "Pengajuan"

    # Styling
    bold = Font(bold=True)
    center = Alignment(horizontal='center', vertical='center')
    thin = Side(style='thin')
    border = Border(left=thin, right=thin, top=thin, bottom=thin)
    header_fill = PatternFill("solid", fgColor="1F4E79")
    header_font = Font(bold=True, color="FFFFFF")

    # Header dokumen
    ws.merge_cells('A1:H1')
    ws['A1'] = pengajuan.lokasi
    ws['A1'].font = Font(bold=True, size=14)
    ws['A1'].alignment = center

    ws.merge_cells('A2:H2')
    ws['A2'] = pengajuan.tanggal.strftime('%A, %d %B %Y')
    ws['A2'].alignment = center

    ws.merge_cells('A3:H3')
    ws['A3'] = f"PENGAJUAN {pengajuan.kategori}"
    ws['A3'].font = Font(bold=True, size=12)
    ws['A3'].alignment = center

    # Tabel header
    headers = ['No', 'Nama Barang', 'Qty', 'Satuan', 'Harga Satuan', 'Subtotal']
    cols = ['A', 'B', 'C', 'D', 'E', 'F']
    for col, h in zip(cols, headers):
        cell = ws[f'{col}5']
        cell.value = h
        cell.font = header_font
        cell.fill = header_fill
        cell.alignment = center
        cell.border = border

    # Data items
    row = 6
    for i, item in enumerate(pengajuan.items, 1):
        ws[f'A{row}'] = i
        ws[f'B{row}'] = item.nama_barang
        ws[f'C{row}'] = item.qty
        ws[f'D{row}'] = item.satuan
        ws[f'E{row}'] = item.harga_satuan
        ws[f'F{row}'] = item.subtotal
        for col in cols:
            ws[f'{col}{row}'].border = border
        ws[f'A{row}'].alignment = center
        ws[f'C{row}'].alignment = center
        ws[f'D{row}'].alignment = center
        row += 1

    # Total
    ws[f'E{row}'] = 'TOTAL'
    ws[f'E{row}'].font = bold
    ws[f'E{row}'].alignment = Alignment(horizontal='right')
    ws[f'F{row}'] = pengajuan.total
    ws[f'F{row}'].font = bold
    ws[f'F{row}'].number_format = '#,##0'

    # Pemasok
    row += 2
    if pengajuan.pemasok:
        p = pengajuan.pemasok
        ws[f'A{row}'] = 'Pemasok'
        ws[f'B{row}'] = f"{p.nama} / {p.bank} / {p.no_rekening}"
        ws[f'A{row}'].font = bold

    # Column widths
    ws.column_dimensions['A'].width = 5
    ws.column_dimensions['B'].width = 35
    ws.column_dimensions['C'].width = 10
    ws.column_dimensions['D'].width = 10
    ws.column_dimensions['E'].width = 15
    ws.column_dimensions['F'].width = 18

    output = io.BytesIO()
    wb.save(output)
    output.seek(0)

    fname = f"Pengajuan_{pengajuan.kategori}_{pengajuan.tanggal.strftime('%Y%m%d')}.xlsx"
    return send_file(output, download_name=fname,
                     mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                     as_attachment=True)


# ─────────────────────────────────────────
# EXPORT – REKAP HARIAN EXCEL (semua kategori, 1 tanggal)
# ─────────────────────────────────────────

@app.route('/export/rekap/<string:tanggal>')
def export_rekap(tanggal):
    from openpyxl import Workbook
    from openpyxl.styles import Font, Alignment, PatternFill, Border, Side

    tgl = datetime.strptime(tanggal, '%Y-%m-%d').date()
    pengajuan_list = PengajuanHeader.query.filter(
        PengajuanHeader.tanggal == tgl
    ).order_by(PengajuanHeader.kategori).all()

    wb = Workbook()
    ws = wb.active
    ws.title = "Rekap Harian"

    bold = Font(bold=True)
    center = Alignment(horizontal='center', vertical='center')
    thin = Side(style='thin')
    border = Border(left=thin, right=thin, top=thin, bottom=thin)
    kat_fill = PatternFill("solid", fgColor="2E75B6")
    kat_font = Font(bold=True, color="FFFFFF", size=11)
    header_fill = PatternFill("solid", fgColor="D6E4F0")

    row = 1
    ws.merge_cells(f'A{row}:G{row}')
    ws[f'A{row}'] = 'SPPG SENDANGREJO'
    ws[f'A{row}'].font = Font(bold=True, size=14)
    ws[f'A{row}'].alignment = center
    row += 1

    ws.merge_cells(f'A{row}:G{row}')
    ws[f'A{row}'] = tgl.strftime('%A, %d %B %Y')
    ws[f'A{row}'].alignment = center
    row += 2

    grand_total = 0

    for pgj in pengajuan_list:
        # Kategori header
        ws.merge_cells(f'A{row}:G{row}')
        ws[f'A{row}'] = pgj.kategori
        ws[f'A{row}'].font = kat_font
        ws[f'A{row}'].fill = kat_fill
        ws[f'A{row}'].alignment = center
        row += 1

        # Column header
        for col, h in zip(['A','B','C','D','E','F'], ['No','Nama Barang','Qty','Satuan','Harga','Subtotal']):
            ws[f'{col}{row}'] = h
            ws[f'{col}{row}'].font = Font(bold=True)
            ws[f'{col}{row}'].fill = PatternFill("solid", fgColor="D6E4F0")
            ws[f'{col}{row}'].border = border
            ws[f'{col}{row}'].alignment = center
        row += 1

        for i, item in enumerate(pgj.items, 1):
            ws[f'A{row}'] = i
            ws[f'B{row}'] = item.nama_barang
            ws[f'C{row}'] = item.qty
            ws[f'D{row}'] = item.satuan
            ws[f'E{row}'] = item.harga_satuan
            ws[f'F{row}'] = item.subtotal
            for col in ['A','B','C','D','E','F']:
                ws[f'{col}{row}'].border = border
            row += 1

        ws[f'E{row}'] = 'Subtotal'
        ws[f'E{row}'].font = bold
        ws[f'F{row}'] = pgj.total
        ws[f'F{row}'].font = bold
        ws[f'F{row}'].number_format = '#,##0'
        grand_total += pgj.total

        if pgj.pemasok:
            p = pgj.pemasok
            ws[f'G{row}'] = f"{p.nama} / {p.bank} / {p.no_rekening}"

        row += 2

    # Grand total
    ws[f'E{row}'] = 'GRAND TOTAL'
    ws[f'E{row}'].font = Font(bold=True, size=12)
    ws[f'F{row}'] = grand_total
    ws[f'F{row}'].font = Font(bold=True, size=12)
    ws[f'F{row}'].number_format = '#,##0'

    ws.column_dimensions['A'].width = 5
    ws.column_dimensions['B'].width = 35
    ws.column_dimensions['C'].width = 10
    ws.column_dimensions['D'].width = 10
    ws.column_dimensions['E'].width = 15
    ws.column_dimensions['F'].width = 18
    ws.column_dimensions['G'].width = 40

    output = io.BytesIO()
    wb.save(output)
    output.seek(0)

    fname = f"Rekap_Harian_{tgl.strftime('%Y%m%d')}.xlsx"
    return send_file(output, download_name=fname,
                     mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                     as_attachment=True)


# ─────────────────────────────────────────
# EXPORT – PDF
# ─────────────────────────────────────────

@app.route('/export/pdf/<int:pid>')
def export_pdf(pid):
    try:
        from reportlab.lib.pagesizes import A4
        from reportlab.lib import colors
        from reportlab.lib.units import cm
        from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
        from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
        from reportlab.lib.enums import TA_CENTER, TA_RIGHT

        pengajuan = PengajuanHeader.query.get_or_404(pid)
        output = io.BytesIO()
        doc = SimpleDocTemplate(output, pagesize=A4,
                                topMargin=1.5*cm, bottomMargin=1.5*cm,
                                leftMargin=2*cm, rightMargin=2*cm)

        styles = getSampleStyleSheet()
        title_style = ParagraphStyle('title', parent=styles['Normal'],
                                     fontSize=14, alignment=TA_CENTER, fontName='Helvetica-Bold')
        sub_style = ParagraphStyle('sub', parent=styles['Normal'],
                                   fontSize=11, alignment=TA_CENTER)
        normal = styles['Normal']

        story = []
        story.append(Paragraph(pengajuan.lokasi, title_style))
        story.append(Paragraph(pengajuan.tanggal.strftime('%A, %d %B %Y'), sub_style))
        story.append(Paragraph(f"PENGAJUAN {pengajuan.kategori}", sub_style))
        story.append(Spacer(1, 0.5*cm))

        # Tabel
        data = [['No', 'Nama Barang', 'Qty', 'Satuan', 'Harga Satuan', 'Subtotal']]
        for i, item in enumerate(pengajuan.items, 1):
            data.append([
                str(i),
                item.nama_barang,
                f"{item.qty:g}",
                item.satuan,
                f"Rp {item.harga_satuan:,.0f}",
                f"Rp {item.subtotal:,.0f}"
            ])
        data.append(['', '', '', '', 'TOTAL', f"Rp {pengajuan.total:,.0f}"])

        col_widths = [1*cm, 6.5*cm, 2*cm, 1.8*cm, 3*cm, 3.2*cm]
        table = Table(data, colWidths=col_widths, repeatRows=1)
        table.setStyle(TableStyle([
            ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#1F4E79')),
            ('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
            ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
            ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
            ('ALIGN', (1, 1), (1, -1), 'LEFT'),
            ('ALIGN', (4, 1), (5, -1), 'RIGHT'),
            ('FONTNAME', (4, -1), (5, -1), 'Helvetica-Bold'),
            ('GRID', (0, 0), (-1, -2), 0.5, colors.grey),
            ('ROWBACKGROUNDS', (0, 1), (-1, -2), [colors.white, colors.HexColor('#EBF5FB')]),
            ('TOPPADDING', (0, 0), (-1, -1), 4),
            ('BOTTOMPADDING', (0, 0), (-1, -1), 4),
        ]))
        story.append(table)
        story.append(Spacer(1, 0.5*cm))

        if pengajuan.pemasok:
            p = pengajuan.pemasok
            story.append(Paragraph(
                f"<b>Pemasok:</b> {p.nama} / {p.bank} / {p.no_rekening}", normal))

        doc.build(story)
        output.seek(0)
        fname = f"Pengajuan_{pengajuan.kategori}_{pengajuan.tanggal.strftime('%Y%m%d')}.pdf"
        return send_file(output, download_name=fname,
                         mimetype='application/pdf', as_attachment=True)
    except ImportError:
        return "ReportLab tidak terinstall. Jalankan: pip install reportlab", 500


# ─────────────────────────────────────────
# API – DASHBOARD DATA
# ─────────────────────────────────────────

@app.route('/api/dashboard')
def api_dashboard():
    tgl_str = request.args.get('tanggal', date.today().strftime('%Y-%m-%d'))
    tgl = datetime.strptime(tgl_str, '%Y-%m-%d').date()

    rekap = db.session.query(
        PengajuanHeader.kategori,
        db.func.sum(PengajuanHeader.total).label('total')
    ).filter(PengajuanHeader.tanggal == tgl
    ).group_by(PengajuanHeader.kategori).all()

    grand_total = sum(r.total for r in rekap)
    return jsonify({
        'tanggal': tgl_str,
        'grand_total': grand_total,
        'rekap': [{'kategori': r.kategori, 'total': r.total} for r in rekap]
    })



# ─────────────────────────────────────────
# ROUTES – PENGATURAN
# ─────────────────────────────────────────

@app.route('/pengaturan')
def pengaturan():
    settings = {r.kunci: r.nilai for r in Pengaturan.query.all()}
    dapur_list = Dapur.query.order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    dapur_json = [d.to_dict() for d in dapur_list]
    kategori_list = KategoriPengajuan.query.order_by(
        KategoriPengajuan.jenis.asc(), KategoriPengajuan.urutan.asc(), KategoriPengajuan.nama.asc()).all()
    kategori_json = [k.to_dict() for k in kategori_list]
    return render_template('pengaturan.html',
                           settings=settings,
                           dapur_json=dapur_json,
                           kategori_json=kategori_json,
                           admin_fee=ADMIN_FEE_NON_BRI)

@app.route('/api/pengaturan', methods=['GET'])
def api_pengaturan_get():
    rows = Pengaturan.query.all()
    return jsonify({r.kunci: r.nilai for r in rows})

@app.route('/api/pengaturan', methods=['POST'])
def api_pengaturan_save():
    data = request.json or {}
    for kunci, nilai in data.items():
        set_setting(kunci, str(nilai).strip())
    return jsonify({'ok': True, 'saved': list(data.keys())})


# ─────────────────────────────────────────
# ROUTES – NOTIFIKASI TEST & BROADCAST
# ─────────────────────────────────────────

@app.route('/api/notifications/test', methods=['POST'])
def api_notifications_test():
    """Menguji koneksi pengiriman pesan notifikasi ke Telegram atau WhatsApp"""
    data = request.json or {}
    channel = data.get('channel', 'telegram')
    now_str = datetime.now().strftime('%d/%m/%Y %H:%M:%S')

    if channel == 'telegram':
        token = data.get('token') or get_setting('notif_telegram_token', '')
        chat_id = data.get('chat_id') or get_setting('notif_telegram_chat_id', '')
        if not token or not chat_id:
            return jsonify({'ok': False, 'error': 'Token Bot dan Chat ID Telegram wajib diisi.'}), 400
        
        text = (f"🔔 <b>UJI COBA NOTIFIKASI TELEGRAM</b>\n\n"
                f"Sistem Pengajuan & Keuangan Dapur SPPG berhasil terhubung ke akun/grup Telegram Anda!\n"
                f"• <b>Waktu:</b> {now_str} WIB\n"
                f"• <b>Status:</b> Koneksi Aktif & Siap Digunakan.")
        try:
            ok, msg = send_telegram_msg(token, chat_id, text)
            return jsonify({'ok': True, 'message': 'Pesan uji coba berhasil terkirim ke Telegram!'})
        except Exception as e:
            return jsonify({'ok': False, 'error': f"Gagal mengirim ke Telegram: {str(e)}"}), 400

    elif channel == 'whatsapp':
        provider = data.get('provider') or get_setting('notif_wa_provider', 'fonnte')
        url = data.get('url') or get_setting('notif_wa_url', 'https://api.fonnte.com/send')
        token = data.get('token') or get_setting('notif_wa_token', '')
        target = data.get('target') or get_setting('notif_wa_target', '')
        if not token or not target:
            return jsonify({'ok': False, 'error': 'API Token dan Nomor Tujuan WhatsApp wajib diisi.'}), 400

        text = (f"🔔 *UJI COBA NOTIFIKASI WHATSAPP*\n\n"
                f"Sistem Pengajuan & Keuangan Dapur SPPG berhasil terhubung ke WhatsApp Anda!\n"
                f"• *Waktu:* {now_str} WIB\n"
                f"• *Status:* Koneksi Aktif & Siap Digunakan.")
        try:
            ok, msg = send_whatsapp_msg(provider, url, token, target, text)
            return jsonify({'ok': True, 'message': 'Pesan uji coba berhasil terkirim ke WhatsApp!'})
        except Exception as e:
            return jsonify({'ok': False, 'error': f"Gagal mengirim ke WhatsApp: {str(e)}"}), 400

    return jsonify({'ok': False, 'error': 'Channel tidak valid'}), 400


@app.route('/api/notifications/send-daily-summary', methods=['POST'])
def api_notifications_send_daily_summary():
    """Broadcast rekap keuangan hari ini ke Telegram & WhatsApp"""
    today = date.today()
    req_data = request.json or {}
    dapur_id = req_data.get('dapur_id')

    # Query Belanja
    qb = CatatanBelanjaPemasok.query.filter_by(tanggal=today)
    if dapur_id:
        qb = qb.filter_by(dapur_id=int(dapur_id))
    b_rows = qb.all()

    # Query Operasional
    qo = PengajuanOperasional.query.filter_by(tanggal=today)
    if dapur_id:
        qo = qo.filter_by(dapur_id=int(dapur_id))
    o_rows = qo.all()

    total_b = sum(r.total_belanja or 0 for r in b_rows)
    total_o = sum(r.nominal or 0 for r in o_rows)
    grand = total_b + total_o
    total_tf = sum(r.total_transfer or 0 for r in b_rows) + sum(r.total_transfer or 0 for r in o_rows)
    total_admin = sum(r.biaya_admin or 0 for r in b_rows) + sum(r.biaya_admin or 0 for r in o_rows)
    
    valid_b = sum(1 for r in b_rows if r.divalidasi)
    valid_o = sum(1 for r in o_rows if r.status == 'divalidasi')
    total_tx = len(b_rows) + len(o_rows)
    total_valid = valid_b + valid_o

    nama_dapur = get_setting('nama_dapur', 'SPPG SENDANGREJO')
    if dapur_id:
        d_obj = Dapur.query.get(dapur_id)
        if d_obj:
            nama_dapur = d_obj.nama

    details = [
        ("Unit Dapur", nama_dapur),
        ("Tanggal", today.strftime('%A, %d %B %Y')),
        ("Belanja Bahan", f"Rp {total_b:,.0f} ({len(b_rows)} nota)"),
        ("Biaya Operasional", f"Rp {total_o:,.0f} ({len(o_rows)} nota)"),
        ("✂️ Potongan Admin", f"Rp {total_admin:,.0f}"),
        ("🏦 Total Transfer Net", f"Rp {total_tf:,.0f}"),
        ("💵 GRAND TOTAL", f"Rp {grand:,.0f}"),
        ("Status Validasi", f"{total_valid} dari {total_tx} transaksi telah divalidasi"),
    ]

    kirim_notifikasi_async(f"📊 REKAP KEUANGAN HARIAN — {nama_dapur}", details, "Laporan resmi Sistem Pengajuan & Keuangan SPPG")
    return jsonify({
        'ok': True,
        'message': f'Rekap hari ini ({today.strftime("%d/%m/%Y")}) berhasil dikirim ke antrean notifikasi!'
    })


# ─────────────────────────────────────────
# ROUTES – AUDIT TRAIL, VERSIONING & BACKUP
# ─────────────────────────────────────────


@app.route('/api/notifications/send-validation-summary', methods=['POST'])
def api_notifications_send_validation_summary():
    """
    Kirim notifikasi WA rekap VALIDASI SELESAI secara konsolidasi.
    Body JSON: { dapur_id: int|None, tanggal: 'YYYY-MM-DD'|None }
    """
    if get_setting('notif_rekap_validasi_aktif', '1') == '0':
        return jsonify({'ok': False, 'error': 'Fitur Notifikasi Rekap Validasi dinonaktifkan di Pengaturan.'}), 403

    wa_aktif = get_setting('notif_wa_aktif', '0') == '1'
    if not wa_aktif:
        return jsonify({'ok': False, 'error': 'Notifikasi WhatsApp belum aktif. Aktifkan terlebih dahulu di Pengaturan.'}), 400

    req_data = request.json or {}
    dapur_id = req_data.get('dapur_id')
    tgl_str  = req_data.get('tanggal') or date.today().strftime('%Y-%m-%d')

    try:
        tgl_date = datetime.strptime(tgl_str, '%Y-%m-%d').date()
    except ValueError:
        return jsonify({'ok': False, 'error': f'Format tanggal tidak valid: {tgl_str}'}), 400

    # Query belanja yang sudah divalidasi
    qb = CatatanBelanjaPemasok.query.filter_by(tanggal=tgl_date, divalidasi=True)
    if dapur_id:
        qb = qb.filter_by(dapur_id=int(dapur_id))
    b_rows = qb.order_by(CatatanBelanjaPemasok.id.asc()).all()

    # Query operasional yang sudah divalidasi
    qo = PengajuanOperasional.query.filter_by(tanggal=tgl_date, status='divalidasi')
    if dapur_id:
        qo = qo.filter_by(dapur_id=int(dapur_id))
    o_rows = qo.order_by(PengajuanOperasional.id.asc()).all()

    total_items = len(b_rows) + len(o_rows)
    if total_items == 0:
        return jsonify({'ok': False, 'error': 'Tidak ada transaksi yang sudah divalidasi pada tanggal ini.'}), 404

    # Nama dapur & saldo VA
    nama_dapur = get_setting('nama_dapur', 'SPPG')
    d_obj = None
    saldo_va = 0.0
    if dapur_id:
        d_obj = Dapur.query.get(int(dapur_id))
        if d_obj:
            nama_dapur = d_obj.nama
            saldo_va   = float(d_obj.saldo_va or 0.0)

    # Bangun isi pesan WA
    HARI = ['Senin','Selasa','Rabu','Kamis','Jumat','Sabtu','Minggu']
    tgl_label = f"{HARI[tgl_date.weekday()]}, {tgl_date.strftime('%d/%m/%Y')}"

    def rp(n): return f"Rp {float(n or 0):,.0f}"

    lines = []
    lines.append(f"\u2705 *REKAP VALIDASI SELESAI*")
    lines.append(f"\U0001f3e0 *{nama_dapur}*")
    lines.append(f"\U0001f4c5 {tgl_label}")
    lines.append("")

    total_belanja_va = 0.0
    total_admin_all  = 0.0
    total_transfer   = 0.0
    validator_names  = set()

    no = 1
    if b_rows:
        lines.append("*\u2014 BELANJA BAHAN \u2014*")
        for r in b_rows:
            penerima = r.nama_penerima or (r.pemasok.nama if r.pemasok else '-')
            adm = float(r.biaya_admin or 0)
            tb  = float(r.total_belanja or 0)
            tf  = float(r.total_transfer or 0)
            total_belanja_va += tb
            total_admin_all  += adm
            total_transfer   += tf
            if r.validated_by:
                validator_names.add(r.validated_by)
            adm_txt = f" | Adm: {rp(adm)}" if adm > 0 else " | BRI (Gratis)"
            lines.append(f"{no}. {penerima} ({r.kategori or '-'})")
            lines.append(f"   {rp(tb)}{adm_txt} \u2192 Trf: {rp(tf)}")
            no += 1

    if o_rows:
        if b_rows:
            lines.append("")
        lines.append("*\u2014 OPERASIONAL \u2014*")
        for r in o_rows:
            adm = float(r.biaya_admin or 0)
            nom = float(r.nominal or 0)
            tf  = float(r.total_transfer or 0)
            total_belanja_va += nom
            total_admin_all  += adm
            total_transfer   += tf
            if r.validated_by:
                validator_names.add(r.validated_by)
            adm_txt = f" | Adm: {rp(adm)}" if adm > 0 else " | BRI (Gratis)"
            lines.append(f"{no}. {r.nama_penerima} ({r.kategori})")
            lines.append(f"   {rp(nom)}{adm_txt} \u2192 Trf: {rp(tf)}")
            no += 1

    lines.append("")
    lines.append("\u2500" * 21)
    lines.append(f"\U0001f4b0 *Total Belanja (Potong VA):* {rp(total_belanja_va)}")
    lines.append(f"\u2702\ufe0f *Total Biaya Admin:* {rp(total_admin_all)}")
    lines.append(f"\U0001f3e6 *Total Net Transfer:* {rp(total_transfer)}")
    if d_obj:
        lines.append(f"\U0001f4b3 *Sisa Saldo VA:* {rp(saldo_va)}")
    lines.append("")
    lines.append(f"\U0001f4cb *Total Transaksi:* {total_items} item")
    if validator_names:
        lines.append(f"\U0001f464 *Divalidasi oleh:* {', '.join(sorted(validator_names))}")
    lines.append(f"\u23f0 *Waktu Kirim:* {datetime.now().strftime('%d/%m/%Y %H:%M')} WIB")
    lines.append("")
    lines.append("_Rekap otomatis dari Sistem Pengajuan & Keuangan SPPG_")

    wa_text = "\n".join(lines)

    # Kirim ke target: Admin Pusat + Ka SPPG + Akuntan setiap dapur yang terlibat
    wa_prov   = get_setting('notif_wa_provider', 'fonnte')
    wa_url    = get_setting('notif_wa_url', 'https://api.fonnte.com/send')
    wa_tok    = get_setting('notif_wa_token', '')
    pusat_tgt = get_setting('notif_wa_target', '').strip()

    if not wa_tok:
        return jsonify({'ok': False, 'error': 'API Token WhatsApp belum diatur di Pengaturan.'}), 400

    def _add_targets(target_list, nomor_str):
        """Tambah nomor (pisah koma) ke target_list jika belum ada."""
        for t in (nomor_str or '').split(','):
            tc = t.strip()
            if tc and tc not in target_list:
                target_list.append(tc)

    def _tambah_nomor_dapur(dapur_obj, target_list):
        """Tambah no_wa & no_wa_akuntan dari dapur jika toggle validasi aktif."""
        if dapur_obj is None:
            return
        # SQLite menyimpan boolean sebagai 0/1; None = default aktif
        aktif = dapur_obj.notif_validasi_aktif
        if aktif is None or aktif not in (False, 0):
            _add_targets(target_list, dapur_obj.no_wa or '')
            _add_targets(target_list, dapur_obj.no_wa_akuntan or '')

    target_list = []
    # 1. Admin Pusat / Bendahara Utama
    _add_targets(target_list, pusat_tgt)

    # 2. Nomor WA per-dapur (Ka SPPG + Akuntan)
    if d_obj:
        # Dapur spesifik sudah diketahui dari query awal
        _tambah_nomor_dapur(d_obj, target_list)
    else:
        # "Semua Dapur" — kumpulkan dapur_id unik dari semua baris tervalidasi
        dapur_ids_terlibat = set()
        for r in b_rows:
            if r.dapur_id:
                dapur_ids_terlibat.add(r.dapur_id)
        for r in o_rows:
            if r.dapur_id:
                dapur_ids_terlibat.add(r.dapur_id)
        for did in sorted(dapur_ids_terlibat):
            try:
                dpr = Dapur.query.get(did)
                _tambah_nomor_dapur(dpr, target_list)
            except Exception as ex:
                print(f"[REKAP VALIDASI] Gagal ambil nomor dapur {did}: {ex}")

    if not target_list:
        return jsonify({'ok': False, 'error': 'Tidak ada nomor WA tujuan yang dikonfigurasi (Admin Pusat, Ka SPPG, atau Akuntan).'}), 400

    sent   = 0
    errors = []
    for num in target_list:
        try:
            ok_send, msg_send = send_whatsapp_msg(wa_prov, wa_url, wa_tok, num, wa_text)
            if ok_send:
                sent += 1
            else:
                errors.append(f"{num}: {msg_send}")
        except Exception as ex:
            errors.append(f"{num}: {str(ex)}")

    if sent == 0:
        return jsonify({'ok': False, 'error': f'Gagal mengirim ke semua nomor. Detail: {"; ".join(errors)}'}), 500

    return jsonify({
        'ok': True,
        'message': f'Rekap validasi berhasil dikirim ke {sent} nomor WA! ({total_items} item \u2014 {tgl_label})',
        'sent': sent,
        'total_items': total_items,
        'preview': wa_text[:400] + ('...' if len(wa_text) > 400 else '')
    })


@app.route('/audit-log')
def audit_log_view():
    """Halaman UI Audit Trail, Versioning, dan Backup Otomatis"""
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        flash('Akses ditolak: Hanya Super Admin dan Validator yang dapat mengakses Audit Trail.', 'danger')
        return redirect(url_for('index'))
    
    dapur_list = Dapur.query.order_by(Dapur.urutan.asc(), Dapur.nama.asc()).all()
    return render_template('audit_log.html', dapur_list=dapur_list)


@app.route('/api/audit-logs', methods=['GET'])
def api_audit_logs_list():
    """API list data audit logs dengan filter tanggal, modul, aksi, dan dapur"""
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Unauthorized'}), 403

    tgl_dari_str = request.args.get('tgl_dari', '')
    tgl_sampai_str = request.args.get('tgl_sampai', '')
    modul = request.args.get('modul', '').strip().upper()
    aksi = request.args.get('aksi', '').strip().upper()
    dapur_id = request.args.get('dapur_id', '').strip()
    limit = int(request.args.get('limit', 150))

    q = AuditLog.query

    if tgl_dari_str:
        try:
            tgl_dari = datetime.strptime(tgl_dari_str, '%Y-%m-%d')
            q = q.filter(AuditLog.waktu >= tgl_dari)
        except ValueError:
            pass
    if tgl_sampai_str:
        try:
            tgl_sampai = datetime.strptime(tgl_sampai_str, '%Y-%m-%d') + timedelta(days=1)
            q = q.filter(AuditLog.waktu < tgl_sampai)
        except ValueError:
            pass

    if modul:
        q = q.filter(AuditLog.modul == modul)
    if aksi:
        q = q.filter(AuditLog.aksi == aksi)
    if dapur_id:
        if dapur_id in ['none', '0']:
            q = q.filter(AuditLog.dapur_id == None)
        else:
            q = q.filter(AuditLog.dapur_id == int(dapur_id))

    logs = q.order_by(AuditLog.waktu.desc()).limit(limit).all()
    
    # Hitung ringkasan statistik
    total_logs = AuditLog.query.count()
    total_creates = AuditLog.query.filter_by(aksi='CREATE').count()
    total_updates = AuditLog.query.filter_by(aksi='UPDATE').count()
    total_deletes = AuditLog.query.filter_by(aksi='DELETE').count()
    total_restored = AuditLog.query.filter_by(aksi='RESTORE').count()

    return jsonify({
        'logs': [item.to_dict() for item in logs],
        'stats': {
            'total': total_logs,
            'create': total_creates,
            'update': total_updates,
            'delete': total_deletes,
            'restored': total_restored
        }
    })


@app.route('/api/audit-logs/restore/<int:log_id>', methods=['POST'])
def api_audit_logs_restore(log_id):
    """
    Restore / Undo data yang sebelumnya dihapus atau dibatalkan berdasarkan snapshot data_lama.
    Mendukung modul: BELANJA, OPERASIONAL.
    """
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Hanya Validator atau Super Admin yang berhak memulihkan data.'}), 403

    log_entry = AuditLog.query.get_or_404(log_id)
    if not log_entry.can_restore:
        return jsonify({'error': 'Entri log ini tidak memiliki rekaman data untuk dipulihkan.'}), 400
    if log_entry.is_restored:
        return jsonify({'error': 'Data dari log ini sudah pernah dipulihkan sebelumnya.'}), 400

    old_data = log_entry.data_lama
    if not old_data or not isinstance(old_data, dict):
        return jsonify({'error': 'Snapshot data lama tidak valid.'}), 400

    restored_id = None
    try:
        if log_entry.modul == 'BELANJA':
            tgl = datetime.strptime(old_data.get('tanggal'), '%Y-%m-%d').date() if old_data.get('tanggal') else date.today()
            total_belanja = float(old_data.get('total_belanja') or 0)
            biaya_admin = float(old_data.get('biaya_admin') or 0)
            total_transfer = float(old_data.get('total_transfer') or 0)

            rec = CatatanBelanjaPemasok(
                pemasok_id=old_data.get('pemasok_id'),
                dapur_id=old_data.get('dapur_id'),
                tanggal=tgl,
                kategori=old_data.get('kategori', ''),
                nama_penerima=old_data.get('nama_penerima', ''),
                bank_penerima=old_data.get('bank_penerima', ''),
                no_rekening_penerima=old_data.get('no_rekening_penerima', ''),
                total_belanja=total_belanja,
                biaya_admin=biaya_admin,
                total_transfer=total_transfer,
                keterangan=f"[RESTORED] {old_data.get('keterangan', '')}".strip(),
                diajukan=old_data.get('diajukan', False),
                divalidasi=old_data.get('divalidasi', False),
                validated_by=old_data.get('validated_by', '')
            )
            db.session.add(rec)
            db.session.commit()
            restored_id = rec.id

        elif log_entry.modul == 'OPERASIONAL':
            tgl = datetime.strptime(old_data.get('tanggal'), '%Y-%m-%d').date() if old_data.get('tanggal') else date.today()
            nomor = old_data.get('nomor', '')
            if not nomor or PengajuanOperasional.query.filter_by(nomor=nomor).first():
                nomor = generate_nomor_operasional()
            nominal = float(old_data.get('nominal') or 0)
            biaya_admin = float(old_data.get('biaya_admin') or 0)
            total_transfer = float(old_data.get('total_transfer') or 0)

            rec = PengajuanOperasional(
                nomor=nomor,
                dapur_id=old_data.get('dapur_id'),
                pemasok_id=old_data.get('pemasok_id'),
                nama_penerima=old_data.get('nama_penerima', ''),
                bank_penerima=old_data.get('bank_penerima', ''),
                no_rekening_penerima=old_data.get('no_rekening_penerima', ''),
                tanggal=tgl,
                kategori=old_data.get('kategori', ''),
                keperluan=old_data.get('keperluan', ''),
                nominal=nominal,
                biaya_admin=biaya_admin,
                total_transfer=total_transfer,
                status=old_data.get('status', 'diajukan'),
                keterangan=f"[RESTORED] {old_data.get('keterangan', '')}".strip()
            )
            db.session.add(rec)
            db.session.commit()
            restored_id = rec.id
        else:
            return jsonify({'error': f'Pemulihan otomatis belum didukung untuk modul {log_entry.modul}.'}), 400

        # Tandai log ini sudah dipulihkan
        log_entry.is_restored = True
        db.session.commit()

        # Buat audit log baru untuk aksi pemulihan
        log_audit(
            aksi='RESTORE',
            modul=log_entry.modul,
            ref_id=restored_id,
            deskripsi=f"Pemulihan data {log_entry.modul} dari riwayat log #{log_entry.id} (Ref Awal #{log_entry.ref_id})",
            data_baru=old_data
        )

        return jsonify({
            'success': True,
            'message': f'Data {log_entry.modul} berhasil dipulihkan dengan ID #{restored_id}!',
            'restored_id': restored_id
        })

    except Exception as e:
        db.session.rollback()
        return jsonify({'error': f'Gagal memulihkan data: {str(e)}'}), 500


@app.route('/api/system/backup-db', methods=['GET'])
def api_system_backup_db():
    """Download cadangan berkas database SQLite (.db atau .zip terenkapsulasi)"""
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Unauthorized'}), 403

    db_path = os.path.join(app.instance_path, 'pengajuan.db')
    if not os.path.exists(db_path):
        return jsonify({'error': 'File database tidak ditemukan di instance path.'}), 404

    # Catat audit log BACKUP
    log_audit(
        aksi='BACKUP',
        modul='SYSTEM',
        deskripsi=f"Pengunduhan cadangan berkas database pengajuan.db oleh {curr.nama_lengkap if curr else 'User'}"
    )

    now_str = datetime.now().strftime('%Y%m%d_%H%M%S')
    backup_filename = f"sppg_backup_{now_str}.db"

    return send_file(
        db_path,
        as_attachment=True,
        download_name=backup_filename,
        mimetype='application/x-sqlite3'
    )


@app.route('/api/system/auto-backup', methods=['POST'])
def api_system_auto_backup():
    """Membuat salinan cadangan snapshot DB lokal ke folder backups/ dengan timestamp"""
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Unauthorized'}), 403

    try:
        db_path = os.path.join(app.instance_path, 'pengajuan.db')
        if not os.path.exists(db_path):
            return jsonify({'error': 'File database tidak ditemukan.'}), 404

        backup_dir = os.path.join(app.root_path, 'backups')
        os.makedirs(backup_dir, exist_ok=True)

        now = datetime.now()
        timestamp = now.strftime('%Y%m%d_%H%M%S')
        target_name = f"backup_pengajuan_{timestamp}.db"
        target_path = os.path.join(backup_dir, target_name)

        # Salin file SQLite secara aman
        shutil.copy2(db_path, target_path)

        file_size_kb = round(os.path.getsize(target_path) / 1024, 2)

        # Catat audit log
        log_audit(
            aksi='BACKUP',
            modul='SYSTEM',
            deskripsi=f"Snapshot lokal otomatis berhasil dibuat: {target_name} ({file_size_kb} KB)"
        )

        return jsonify({
            'success': True,
            'message': f'Cadangan database snapshot lokal berhasil dibuat!',
            'filename': target_name,
            'size_kb': file_size_kb,
            'timestamp': now.strftime('%d/%m/%Y %H:%M:%S')
        })
    except Exception as e:
        return jsonify({'error': f'Gagal membuat cadangan database: {str(e)}'}), 500


@app.route('/api/system/backups-list', methods=['GET'])
def api_system_backups_list():
    """Melihat daftar file backup snapshot yang tersimpan di server"""
    curr = get_current_user()
    if curr and curr.role not in ['super_admin', 'validator']:
        return jsonify({'error': 'Unauthorized'}), 403

    backup_dir = os.path.join(app.root_path, 'backups')
    files_list = []
    if os.path.exists(backup_dir):
        for f in sorted(os.listdir(backup_dir), reverse=True):
            if f.endswith('.db') or f.endswith('.zip'):
                p = os.path.join(backup_dir, f)
                stat = os.stat(p)
                files_list.append({
                    'filename': f,
                    'size_kb': round(stat.st_size / 1024, 2),
                    'created_at': datetime.fromtimestamp(stat.st_mtime).strftime('%d/%m/%Y %H:%M:%S')
                })

    return jsonify({'backups': files_list})


@app.route('/api/system/restore-db', methods=['POST'])
def api_system_restore_db():
    """
    Restore database dari upload file .db atau dari snapshot yang tersimpan di server.
    Memerlukan hak akses Super Admin dan konfirmasi password untuk keamanan.
    """
    import sqlite3
    curr = get_current_user()
    if not curr:
        return jsonify({'error': 'Silakan login terlebih dahulu.'}), 401

    if curr.role != 'super_admin':
        return jsonify({'error': 'Akses ditolak: Hanya Super Admin yang berhak melakukan Restore Database.'}), 403

    password_input = request.form.get('password', '').strip()
    if not password_input or not curr.check_password(password_input):
        return jsonify({'error': 'Password konfirmasi salah. Pemulihan database dibatalkan.'}), 400

    db_path = os.path.join(app.instance_path, 'pengajuan.db')
    backup_dir = os.path.join(app.root_path, 'backups')
    os.makedirs(backup_dir, exist_ok=True)

    # 1. Pastikan buat backup pengaman dari database saat ini sebelum ditimpa
    if os.path.exists(db_path):
        ts_safety = datetime.now().strftime('%Y%m%d_%H%M%S')
        safety_backup = os.path.join(backup_dir, f"safety_before_restore_{ts_safety}.db")
        try:
            shutil.copy2(db_path, safety_backup)
        except Exception as e:
            print(f"[RESTORE ERROR] Gagal membuat safety backup: {e}")

    # 2. Cek apakah restore dari file upload atau dari snapshot filename di server
    uploaded_file = request.files.get('backup_file')
    snapshot_filename = request.form.get('snapshot_filename', '').strip()

    source_temp_path = None

    if uploaded_file and uploaded_file.filename:
        fname = uploaded_file.filename
        if not fname.lower().endswith(('.db', '.sqlite', '.sqlite3')):
            return jsonify({'error': 'Format file harus berupa file SQLite (.db, .sqlite, .sqlite3).'}), 400
        
        temp_restore_path = os.path.join(backup_dir, f"temp_upload_restore_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db")
        uploaded_file.save(temp_restore_path)
        source_temp_path = temp_restore_path
    elif snapshot_filename:
        # Restore dari file yang ada di folder backups/
        safe_fname = os.path.basename(snapshot_filename)
        server_file_path = os.path.join(backup_dir, safe_fname)
        if not os.path.exists(server_file_path):
            return jsonify({'error': f'File backup snapshot {safe_fname} tidak ditemukan di server.'}), 404
        source_temp_path = server_file_path
    else:
        return jsonify({'error': 'Silakan pilih file database yang ingin di-restore atau pilih dari daftar snapshot.'}), 400

    # 3. Validasi integritas SQLite dari file sumber
    try:
        conn = sqlite3.connect(source_temp_path)
        cursor = conn.cursor()
        cursor.execute("PRAGMA integrity_check;")
        check_result = cursor.fetchone()
        conn.close()
        if not check_result or check_result[0] != 'ok':
            return jsonify({'error': f'File database korup atau tidak valid: {check_result}'}), 400
    except Exception as e:
        return jsonify({'error': f'File yang diunggah bukan database SQLite yang valid: {str(e)}'}), 400

    # 4. Lakukan penimpaan (Restore) ke database aktif
    try:
        # Tutup koneksi sesi SQLAlchemy yang aktif agar file tidak lock
        db.session.remove()
        db.engine.dispose()

        shutil.copy2(source_temp_path, db_path)

        # Log audit setelah restore berhasil
        log_audit(
            aksi='RESTORE',
            modul='SYSTEM',
            deskripsi=f"Database dipulihkan (Restore) oleh Super Admin {curr.nama_lengkap} dari {uploaded_file.filename if uploaded_file else snapshot_filename}"
        )

        return jsonify({
            'ok': True,
            'message': 'Database berhasil dipulihkan (Restore)! Halaman akan dimuat ulang.'
        })
    except Exception as e:
        return jsonify({'error': f'Gagal menimpa database aktif: {str(e)}'}), 500


@app.route('/api/system/reset-data', methods=['POST'])
def api_system_reset_data():
    """
    Fitur reset data operasional & analitik secara selektif atau total:
    - Modul: va (mutasi & saldo), belanja (catatan belanja pemasok), operasional (pengajuan ops), analitik (pengajuan header & item)
    - Pengamanan: Validasi password Super Admin + Pembuatan Automatic Snapshot Backup sebelum dieksekusi.
    """
    import shutil
    curr = get_current_user()
    if not curr:
        return jsonify({'error': 'Silakan login terlebih dahulu.'}), 401

    # Cek hak akses
    if curr.role != 'super_admin' and not check_permission('fitur_reset_data'):
        return jsonify({'error': 'Akses ditolak. Hanya akun berwenang yang dapat mengeksekusi reset data.'}), 403

    data = request.json or {}
    password_input = data.get('password', '').strip()
    if not password_input:
        return jsonify({'error': 'Password konfirmasi wajib diisi untuk keamanan data.'}), 400

    if not curr.check_password(password_input):
        return jsonify({'error': 'Password konfirmasi salah. Tindakan reset dibatalkan.'}), 400

    modules = data.get('modules', [])
    if not modules or not isinstance(modules, list):
        return jsonify({'error': 'Pilih minimal satu modul data yang ingin di-reset.'}), 400

    target_dapur_id = data.get('target_dapur_id', 'all')
    dapur_obj = None
    if target_dapur_id != 'all':
        try:
            target_dapur_id = int(target_dapur_id)
            dapur_obj = Dapur.query.get(target_dapur_id)
        except Exception:
            target_dapur_id = 'all'

    # 1. Otomatis buat Snapshot Backup Database sebelum reset
    backup_filename = None
    try:
        db_path = os.path.join(app.instance_path, 'pengajuan.db')
        if os.path.exists(db_path):
            backup_dir = os.path.join(app.root_path, 'backups')
            os.makedirs(backup_dir, exist_ok=True)
            timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
            backup_filename = f"backup_before_reset_{timestamp}.db"
            target_backup = os.path.join(backup_dir, backup_filename)
            shutil.copy2(db_path, target_backup)
    except Exception as e:
        print(f"[AUTO BACKUP BEFORE RESET ERROR] {e}")

    results = []

    # 2. Reset Saldo & Mutasi VA
    if 'va' in modules:
        if target_dapur_id == 'all':
            MutasiVA.query.delete()
            Dapur.query.update({'saldo_va': 0.0})
            results.append('Seluruh Mutasi VA & Saldo VA Semua Dapur di-reset ke Rp 0')
        elif dapur_obj:
            MutasiVA.query.filter_by(dapur_id=target_dapur_id).delete()
            dapur_obj.saldo_va = 0.0
            results.append(f'Mutasi & Saldo VA {dapur_obj.nama} di-reset ke Rp 0')

    # 3. Reset Validasi Belanja Pemasok
    if 'belanja' in modules:
        if target_dapur_id == 'all':
            del_count = CatatanBelanjaPemasok.query.delete()
            results.append(f'{del_count} Catatan Belanja Bahan Pemasok dihapus')
        elif dapur_obj:
            del_count = CatatanBelanjaPemasok.query.filter_by(dapur_id=target_dapur_id).delete()
            results.append(f'{del_count} Catatan Belanja Dapur {dapur_obj.nama} dihapus')

    # 4. Reset Pengajuan Biaya Operasional
    if 'operasional' in modules:
        if target_dapur_id == 'all':
            del_count = PengajuanOperasional.query.delete()
            results.append(f'{del_count} Pengajuan Biaya Operasional dihapus')
        elif dapur_obj:
            del_count = PengajuanOperasional.query.filter_by(dapur_id=target_dapur_id).delete()
            results.append(f'{del_count} Pengajuan Operasional Dapur {dapur_obj.nama} dihapus')

    # 5. Reset Dokumen Pengajuan Belanja (Analitik & SCM)
    if 'analitik' in modules or 'scm' in modules:
        if target_dapur_id == 'all':
            PengajuanItem.query.delete()
            del_count = PengajuanHeader.query.delete()
            results.append(f'{del_count} Dokumen Pengajuan Belanja (Analitik & SCM) dihapus')
        elif dapur_obj:
            headers = PengajuanHeader.query.filter(
                db.or_(PengajuanHeader.lokasi == dapur_obj.nama, PengajuanHeader.lokasi == dapur_obj.kode)
            ).all()
            h_ids = [h.id for h in headers]
            if h_ids:
                PengajuanItem.query.filter(PengajuanItem.header_id.in_(h_ids)).delete(synchronize_session=False)
                del_count = PengajuanHeader.query.filter(PengajuanHeader.id.in_(h_ids)).delete(synchronize_session=False)
                results.append(f'{del_count} Dokumen Pengajuan Dapur {dapur_obj.nama} dihapus')
            else:
                results.append(f'Tidak ada dokumen pengajuan terkait {dapur_obj.nama}')

    db.session.commit()

    dapur_label = dapur_obj.nama if dapur_obj else 'Semua Unit Dapur'
    deskripsi_log = f"Reset Data: [{', '.join(modules)}] untuk ({dapur_label}). Hasil: {'; '.join(results)}. Backup cadangan otomatis: {backup_filename or 'None'}"

    log_audit(
        aksi='RESET_DATA',
        modul='SYSTEM',
        deskripsi=deskripsi_log,
        data_lama={'target_dapur': target_dapur_id, 'modules': modules},
        data_baru={'status': 'SUCCESS', 'backup_file': backup_filename}
    )

    return jsonify({
        'ok': True,
        'message': 'Proses reset data berhasil dieksekusi.',
        'details': results,
        'backup_file': backup_filename
    })


# ─────────────────────────────────────────
# INIT DB + SEED DATA
# ─────────────────────────────────────────

def seed_data():
    """Seed pemasok awal dari data Excel"""
    if Pemasok.query.count() > 0:
        return
    pemasok_awal = [
        {'nama': 'SETYARTO', 'bank': 'BRI', 'no_rekening': '359401019576535', 'kategori': 'BERAS'},
        {'nama': 'SURANTO', 'bank': 'BRI', 'no_rekening': '695901004006504', 'kategori': 'SAYUR'},
        {'nama': 'SURANTO', 'bank': 'BRI', 'no_rekening': '695901004006504', 'kategori': 'BUAH'},
        {'nama': 'MARMI', 'bank': 'BNI', 'no_rekening': '2092142535', 'kategori': 'PROTEIN HEWANI'},
        {'nama': 'ABDUL ROKHMAN', 'bank': 'BRI', 'no_rekening': '695901016545536', 'kategori': 'PROTEIN HEWANI'},
        {'nama': 'MULYATI', 'bank': 'BNI', 'no_rekening': '0434285765', 'kategori': 'PROTEIN NABATI'},
        {'nama': 'JOKO SANTOSO', 'bank': 'BRI', 'no_rekening': '006701002096562', 'kategori': 'BUMBON'},
        {'nama': 'IWAN MARSUDIYANTO', 'bank': 'BRI', 'no_rekening': '695901030503532', 'kategori': 'BUMBON'},
    ]
    for data in pemasok_awal:
        db.session.add(Pemasok(**data))
    db.session.commit()
    print("[OK] Data pemasok awal berhasil ditambahkan.")


def seed_pengaturan():
    """Inisialisasi pengaturan default jika belum ada dan pastikan skema database mutakhir."""
    try:
        with db.engine.connect() as conn:
            res = conn.execute(db.text("PRAGMA table_info(dapur)")).fetchall()
            existing_cols = [r[1] for r in res] if res else []
            cols_to_add = [
                ('no_wa_akuntan', 'VARCHAR(50) DEFAULT ""'),
                ('notif_pengajuan_aktif', 'BOOLEAN DEFAULT 1'),
                ('notif_validasi_aktif', 'BOOLEAN DEFAULT 1'),
            ]
            for col_name, col_def in cols_to_add:
                if col_name not in existing_cols:
                    conn.execute(db.text(f"ALTER TABLE dapur ADD COLUMN {col_name} {col_def}"))
                    conn.commit()
    except Exception as e:
        print(f"[DB MIGRATION NOTE] {e}")

    defaults = [
        ('nama_dapur',    'SPPG SENDANGREJO',  'Nama dapur / lokasi yang tampil di seluruh aplikasi'),
        ('alamat_dapur',  '',                   'Alamat lengkap dapur'),
        ('no_telepon',    '',                   'Nomor telepon / WhatsApp'),
        ('admin_fee_bri', '0',                  'Biaya admin transfer ke BRI (Rp)'),
        ('admin_fee_non_bri', '2500',           'Biaya admin transfer ke bank selain BRI (Rp)'),
        ('nama_pengelola','',                   'Nama pengelola / penanggung jawab'),
    ]
    changed = False
    for kunci, nilai_default, desk in defaults:
        if not Pengaturan.query.filter_by(kunci=kunci).first():
            db.session.add(Pengaturan(kunci=kunci, nilai=nilai_default, deskripsi=desk))
            changed = True

    if Dapur.query.count() == 0:
        nama_dapur_def = get_setting('nama_dapur', 'SPPG SENDANGREJO')
        dapur_def = Dapur(nama=nama_dapur_def, kode='SDR', alamat='Sendangrejo', aktif=True, urutan=1)
        db.session.add(dapur_def)
        changed = True

    if KategoriPengajuan.query.count() == 0:
        kategori_barang = [
            ('BERAS', 'Kebutuhan beras pokok', 1),
            ('SAYUR', 'Sayur mayur segar harian', 2),
            ('BUAH', 'Buah-buahan segar', 3),
            ('PROTEIN HEWANI', 'Daging ayam, sapi, ikan, telur', 4),
            ('PROTEIN NABATI', 'Tahu, tempe, olahan kedelai', 5),
            ('BUMBON', 'Bumbu dapur, minyak, garam, gula, rempah', 6),
            ('LAINNYA', 'Kebutuhan bahan pangan lainnya', 7),
        ]
        for nama, desk, urut in kategori_barang:
            db.session.add(KategoriPengajuan(nama=nama, jenis='BARANG', deskripsi=desk, urutan=urut, aktif=True))

        kategori_ops = [
            ('Gas LPG', 'Pembelian / isi ulang tabung gas LPG', 1),
            ('Listrik & Air', 'Tagihan PLN, token listrik, PDAM, air galon', 2),
            ('Bensin & Transportasi', 'BBM belanja pasar dan pengantaran', 3),
            ('Kebersihan & Sanitasi', 'Sabun cuci, kantong sampah, pembersih', 4),
            ('Kemasan & Wadah', 'Plastik mika, box nasi, paper bag, sendok', 5),
            ('Upah & Tenaga Kerja', 'Upah tenaga masak & packing harian', 6),
            ('Perlengkapan Dapur', 'Peralatan masak & perlengkapan pendukung', 7),
            ('Lain-lain', 'Biaya operasional lainnya', 8),
        ]
        for nama, desk, urut in kategori_ops:
            db.session.add(KategoriPengajuan(nama=nama, jenis='OPERASIONAL', deskripsi=desk, urutan=urut, aktif=True))
        changed = True

    # Seed Kategori Pengeluaran Wajib, Opsional & Penggajian
    if KategoriPengeluaranWajib.query.count() == 0:
        default_pw_kat = [
            ('Tagihan', 'WAJIB', 'Tagihan rutin bulanan (listrik PLN, air PDAM, WiFi, sewa gedung, langganan)', 1),
            ('Operasional (Optional)', 'OPSIONAL', 'Biaya operasional tambahan non-VA', 2),
            ('Tukang', 'OPSIONAL', 'Biaya tukang harian/borongan bangunan atau instalasi', 3),
            ('Pemeliharaan Gedung', 'OPSIONAL', 'Perawatan bangunan, renovasi kecil, perbaikan atap, cat, dll', 4),
            ('Pemeliharaan Alat', 'OPSIONAL', 'Servis kompor, reparasi chiller/freezer, blender, instalasi gas', 5),
            ('Lain-lain', 'OPSIONAL', 'Pengeluaran insidental lainnya di luar kategori utama', 6),
            ('Tagihan Suplayer Bahan', 'SUPPLIER', 'Pembayaran bahan pangan dan belanja suplayer', 1),
            ('Penggajian Karyawan', 'PENGGAJIAN', 'Gaji karyawan tetap bulanan', 1),
            ('Honor / Honorarium', 'PENGGAJIAN', 'Honor staf harian, tenaga lepas, dan jasa', 2),
            ('Tunjangan', 'PENGGAJIAN', 'Tunjangan makan, transport, atau kinerja', 3),
            ('Upah Harian', 'PENGGAJIAN', 'Upah tenaga kerja harian/mingguan', 4),
        ]
        for nama, sifat, desc, urut in default_pw_kat:
            db.session.add(KategoriPengeluaranWajib(nama=nama, sifat=sifat, deskripsi=desc, urutan=urut, aktif=True))
        changed = True

    if changed:
        db.session.commit()
        print("[OK] Pengaturan, Dapur, & Kategori default berhasil ditambahkan.")


def seed_users():
    """Seed akun awal: superadmin, validator, admin dapur"""
    if User.query.count() == 0:
        # 1. Super Admin
        admin = User(
            username='admin',
            nama_lengkap='Administrator Pusat',
            role='super_admin',
            dapur_id=None,
            aktif=True
        )
        admin.set_password('admin123')
        db.session.add(admin)

        # 2. Validator / Bendahara
        validator = User(
            username='bendahara',
            nama_lengkap='Bendahara / Validator Pusat',
            role='validator',
            dapur_id=None,
            aktif=True
        )
        validator.set_password('bendahara123')
        db.session.add(validator)

        # 3. Admin Dapur (kaitkan ke dapur pertama jika ada)
        dapur1 = Dapur.query.first()
        ad_dapur = User(
            username='admindapur',
            nama_lengkap='Admin Dapur Sendangrejo',
            role='admin_dapur',
            dapur_id=dapur1.id if dapur1 else None,
            aktif=True
        )
        ad_dapur.set_password('dapur123')
        db.session.add(ad_dapur)

        db.session.commit()
        print("[OK] Akun default (admin, bendahara, admindapur) berhasil dibuat.")


def seed_pagu():
    """Seed data awal pagu anggaran untuk bulan berjalan jika belum ada"""
    today = date.today()
    if PaguAnggaran.query.count() == 0:
        d1 = Dapur.query.filter_by(nama='SPPG SENDANGREJO').first()
        d2 = Dapur.query.filter_by(nama='Dapur Melati').first()

        # Pagu Dapur 1 (Sendangrejo)
        if d1:
            db.session.add(PaguAnggaran(
                dapur_id=d1.id,
                tahun=today.year,
                bulan=today.month,
                nominal_belanja=20000000.0,
                nominal_operasional=5000000.0,
                nominal_total=25000000.0,
                keterangan='Pagu Anggaran SPPG Sendangrejo'
            ))

        # Pagu Dapur 2 (Dapur Melati)
        if d2:
            db.session.add(PaguAnggaran(
                dapur_id=d2.id,
                tahun=today.year,
                bulan=today.month,
                nominal_belanja=12000000.0,
                nominal_operasional=3000000.0,
                nominal_total=15000000.0,
                keterangan='Pagu Anggaran Dapur Melati'
            ))

        # Pagu Konsolidasi Semua Dapur
        db.session.add(PaguAnggaran(
            dapur_id=None,
            tahun=today.year,
            bulan=today.month,
            nominal_belanja=32000000.0,
            nominal_operasional=8000000.0,
            nominal_total=40000000.0,
            keterangan='Pagu Plafon Konsolidasi Yayasan'
        ))

        db.session.commit()
        print("[OK] Pagu anggaran default berhasil ditambahkan.")



@app.errorhandler(500)
def handle_500_error(e):
    import traceback
    err_tb = traceback.format_exc()
    try:
        log_path = os.path.join(BASE_DIR, 'error_debug.log')
        with open(log_path, 'a', encoding='utf-8') as f:
            f.write(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 500 on {request.path}\n{err_tb}\n")
    except Exception:
        pass

    if request.path.startswith('/api/'):
        return jsonify({'error': 'Internal Server Error', 'detail': str(e), 'traceback': err_tb}), 500

    return f"""
    <!DOCTYPE html>
    <html>
    <head><title>System Error (500)</title><meta name="viewport" content="width=device-width, initial-scale=1"></head>
    <body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;background:#f8fafc;padding:30px;color:#1e293b;">
        <div style="max-width:850px;margin:0 auto;background:#fff;border-radius:12px;padding:24px;box-shadow:0 4px 12px rgba(0,0,0,0.08);border:1px solid #e2e8f0;">
            <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
                <span style="background:#fee2e2;color:#dc2626;padding:8px 14px;border-radius:8px;font-weight:bold;font-size:18px;">Error 500</span>
                <h2 style="margin:0;font-size:20px;color:#0f172a;">Terjadi Kesalahan di Server</h2>
            </div>
            <p style="color:#64748b;font-size:14px;margin-bottom:16px;">Halaman: <code>{request.path}</code></p>
            <div style="background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px 16px;color:#991b1b;margin-bottom:16px;">
                <strong>Pesan Kesalahan:</strong> {e}
            </div>
            <details open style="margin-bottom:20px;">
                <summary style="cursor:pointer;font-weight:600;color:#3b82f6;margin-bottom:8px;">Lihat Traceback Lengkap</summary>
                <pre style="background:#1e293b;color:#f8fafc;padding:16px;border-radius:8px;font-size:12px;overflow-x:auto;line-height:1.5;">{err_tb}</pre>
            </details>
            <div style="display:flex;gap:10px;">
                <a href="/" style="background:#2563eb;color:#fff;text-decoration:none;padding:8px 16px;border-radius:6px;font-weight:600;font-size:14px;">Kembali ke Beranda</a>
                <a href="javascript:location.reload()" style="background:#f1f5f9;color:#475569;text-decoration:none;padding:8px 16px;border-radius:6px;font-weight:600;font-size:14px;">Muat Ulang Halaman</a>
            </div>
        </div>
    </body>
    </html>
    """, 500

if __name__ == '__main__':
    with app.app_context():
        try:
            db.create_all()
        except Exception as e:
            print(f"[NOTE] db.create_all: {e}")
        seed_data()
        seed_pengaturan()
        seed_users()
        seed_pagu()
    app.run(debug=True, port=5000)
