# Settings Page Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a `/settings` page under Master Data (superadmin-only) with a System tab (maintenance + env mode toggles) and a Developer Tools tab (truncate operational tables by group, dev mode only).

**Architecture:** Backend gets a new `POST /api/v1/system/truncate` endpoint that accepts a `group` string and deletes rows from operational tables in the correct FK order. Frontend adds `systemApi.truncate()`, a `useTruncate()` hook, a sidebar entry (conditional on `is_superadmin`), and a two-tab settings page — System tab mirrors topbar controls, Developer Tools tab shows danger-zone truncate UI hidden in production.

**Tech Stack:** FastAPI + SQLAlchemy async (backend), Next.js 16 App Router + TanStack Query + Tabler CSS (frontend)

---

## File Map

| File | Action | Responsibility |
|------|--------|----------------|
| `backend/app/schemas/system.py` | Modify | Add `TruncateRequest`, `TruncateResponse` |
| `backend/app/services/system_service.py` | Modify | Add `truncate_group(db, group)` |
| `backend/app/routers/system.py` | Modify | Add `POST /truncate` route |
| `frontend/src/lib/api/system.ts` | Modify | Add `systemApi.truncate()` |
| `frontend/src/lib/hooks/useSystem.ts` | Modify | Add `useTruncate()` hook |
| `frontend/src/components/ui/sidebar.tsx` | Modify | Add Settings entry, superadmin-only |
| `frontend/src/app/(dashboard)/settings/page.tsx` | Create | Thin server page wrapper |
| `frontend/src/app/(dashboard)/settings/settings-client.tsx` | Create | Full settings UI with 2 tabs |

---

## Task 1: Backend schemas — TruncateRequest & TruncateResponse

**Files:**
- Modify: `backend/app/schemas/system.py`

- [ ] **Step 1: Add schemas**

Open `backend/app/schemas/system.py` and append:

```python
from typing import Literal

TruncateGroup = Literal[
    "penjualan",
    "stock",
    "penebusan_penerimaan",
    "expenses_penyetoran",
    "end_to_end",
    "all",
]


class TruncateRequest(BaseModel):
    group: TruncateGroup


class TruncateResponse(BaseModel):
    message: str
    rows_deleted: int
```

The full file after edit:

```python
from typing import Literal

from pydantic import BaseModel


class SystemStatusResponse(BaseModel):
    maintenance_mode: bool
    environment_mode: str  # 'development' or 'production'


class SystemSettingsUpdate(BaseModel):
    maintenance_mode: bool | None = None
    environment_mode: str | None = None  # 'development' or 'production'


TruncateGroup = Literal[
    "penjualan",
    "stock",
    "penebusan_penerimaan",
    "expenses_penyetoran",
    "end_to_end",
    "all",
]


class TruncateRequest(BaseModel):
    group: TruncateGroup


class TruncateResponse(BaseModel):
    message: str
    rows_deleted: int
```

- [ ] **Step 2: Verify import works**

```bash
cd backend && python -c "from app.schemas.system import TruncateRequest, TruncateResponse; print('OK')"
```

Expected output: `OK`

- [ ] **Step 3: Commit**

```bash
git add backend/app/schemas/system.py
git commit -m "feat: add TruncateRequest/TruncateResponse schemas"
```

---

## Task 2: Backend service — truncate_group()

**Files:**
- Modify: `backend/app/services/system_service.py`

- [ ] **Step 1: Add the truncate_group function**

Open `backend/app/services/system_service.py`. Add imports at the top and append the function:

```python
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

from app.repositories import system_repository
from app.schemas.system import TruncateGroup


# Map each group to tables in correct DELETE order (leaf → parent to respect FK constraints)
_TRUNCATE_ORDER: dict[str, list[str]] = {
    "penjualan": ["penjualan_nozzle", "laporan_shift"],
    "stock": [
        "stock_adjustment_item_foto",
        "stock_adjustment_item",
        "stock_adjustment",
    ],
    "penebusan_penerimaan": [
        "penerimaan_foto",
        "penerimaan_item",
        "penerimaan",
        "penebusan_item",
        "penebusan",
    ],
    "expenses_penyetoran": ["expenses", "penyetoran"],
    "end_to_end": ["end_to_end_cycle"],
}

_ALL_ORDER: list[str] = (
    _TRUNCATE_ORDER["penjualan"]
    + _TRUNCATE_ORDER["stock"]
    + _TRUNCATE_ORDER["penebusan_penerimaan"]
    + _TRUNCATE_ORDER["expenses_penyetoran"]
    + _TRUNCATE_ORDER["end_to_end"]
)


async def truncate_group(db: AsyncSession, group: TruncateGroup) -> int:
    """Delete all rows from the given operational table group.

    Returns total number of rows deleted across all affected tables.
    Only callable when environment_mode == 'development' (enforced by router).
    """
    tables = _ALL_ORDER if group == "all" else _TRUNCATE_ORDER[group]
    total = 0
    for table in tables:
        result = await db.execute(text(f"DELETE FROM {table}"))  # noqa: S608
        total += result.rowcount
    await db.commit()
    return total
```

The full file after edit:

```python
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession

from app.repositories import system_repository
from app.schemas.system import TruncateGroup

# Map each group to tables in correct DELETE order (leaf → parent to respect FK constraints)
_TRUNCATE_ORDER: dict[str, list[str]] = {
    "penjualan": ["penjualan_nozzle", "laporan_shift"],
    "stock": [
        "stock_adjustment_item_foto",
        "stock_adjustment_item",
        "stock_adjustment",
    ],
    "penebusan_penerimaan": [
        "penerimaan_foto",
        "penerimaan_item",
        "penerimaan",
        "penebusan_item",
        "penebusan",
    ],
    "expenses_penyetoran": ["expenses", "penyetoran"],
    "end_to_end": ["end_to_end_cycle"],
}

_ALL_ORDER: list[str] = (
    _TRUNCATE_ORDER["penjualan"]
    + _TRUNCATE_ORDER["stock"]
    + _TRUNCATE_ORDER["penebusan_penerimaan"]
    + _TRUNCATE_ORDER["expenses_penyetoran"]
    + _TRUNCATE_ORDER["end_to_end"]
)


async def get_maintenance_mode(db: AsyncSession) -> bool:
    return await system_repository.is_maintenance_mode(db)


async def set_maintenance_mode(
    db: AsyncSession, enabled: bool, user_id: int
) -> None:
    await system_repository.set_config(
        db, system_repository.MAINTENANCE_KEY, str(enabled).lower(), user_id
    )


async def get_env_mode(db: AsyncSession) -> str:
    """Return current environment mode: 'development' or 'production'."""
    config = await system_repository.get_config(db, system_repository.ENV_MODE_KEY)
    return config.value if config else "production"


async def set_env_mode(db: AsyncSession, mode: str, user_id: int) -> None:
    if mode not in ("development", "production"):
        raise ValueError("mode must be 'development' or 'production'")
    await system_repository.set_config(
        db, system_repository.ENV_MODE_KEY, mode, user_id
    )


async def truncate_group(db: AsyncSession, group: TruncateGroup) -> int:
    """Delete all rows from the given operational table group.

    Returns total number of rows deleted across all affected tables.
    Only callable when environment_mode == 'development' (enforced by router).
    """
    tables = _ALL_ORDER if group == "all" else _TRUNCATE_ORDER[group]
    total = 0
    for table in tables:
        result = await db.execute(text(f"DELETE FROM {table}"))  # noqa: S608
        total += result.rowcount
    await db.commit()
    return total
```

- [ ] **Step 2: Verify import works**

```bash
cd backend && python -c "from app.services.system_service import truncate_group; print('OK')"
```

Expected output: `OK`

- [ ] **Step 3: Commit**

```bash
git add backend/app/services/system_service.py
git commit -m "feat: add truncate_group service function"
```

---

## Task 3: Backend router — POST /truncate endpoint

**Files:**
- Modify: `backend/app/routers/system.py`

- [ ] **Step 1: Add the truncate route**

Open `backend/app/routers/system.py`. Add `TruncateRequest` and `TruncateResponse` to the schema import, then append the new route. Full file after edit:

```python
"""System router — endpoints for system-wide settings (maintenance mode)."""

from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.dependencies import get_current_user
from app.models.user import User
from app.schemas.system import SystemSettingsUpdate, SystemStatusResponse, TruncateRequest, TruncateResponse
from app.services import system_service


def _service_error(e: ValueError | PermissionError) -> HTTPException:
    """Convert service-layer ValueError → 404 and PermissionError → 403."""
    if isinstance(e, PermissionError):
        return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(e))
    return HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(e))


router = APIRouter()


@router.get("/status", response_model=SystemStatusResponse)
async def get_status(db: AsyncSession = Depends(get_db)) -> SystemStatusResponse:
    """Public endpoint — no auth required. Returns current system status."""
    maintenance = await system_service.get_maintenance_mode(db)
    env_mode = await system_service.get_env_mode(db)
    return SystemStatusResponse(maintenance_mode=maintenance, environment_mode=env_mode)


@router.patch("/settings", response_model=SystemStatusResponse)
async def update_settings(
    data: SystemSettingsUpdate,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
) -> SystemStatusResponse:
    """Superadmin only — toggle maintenance mode and/or environment mode."""
    if not current_user.is_superadmin:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Only Super Admin can change system settings",
        )
    if data.maintenance_mode is not None:
        await system_service.set_maintenance_mode(
            db, data.maintenance_mode, current_user.id
        )
    if data.environment_mode is not None:
        try:
            await system_service.set_env_mode(db, data.environment_mode, current_user.id)
        except ValueError as e:
            raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(e))
    maintenance = await system_service.get_maintenance_mode(db)
    env_mode = await system_service.get_env_mode(db)
    return SystemStatusResponse(maintenance_mode=maintenance, environment_mode=env_mode)


@router.post("/truncate", response_model=TruncateResponse)
async def truncate_tables(
    data: TruncateRequest,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(get_current_user),
) -> TruncateResponse:
    """Superadmin only, development mode only — truncate operational table group."""
    if not current_user.is_superadmin:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Only Super Admin can truncate tables",
        )
    env_mode = await system_service.get_env_mode(db)
    if env_mode != "development":
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Truncate is only allowed in development mode",
        )
    rows_deleted = await system_service.truncate_group(db, data.group)
    return TruncateResponse(
        message=f"Truncated '{data.group}' successfully",
        rows_deleted=rows_deleted,
    )
```

- [ ] **Step 2: Start backend and verify endpoint appears in docs**

```bash
cd backend && uvicorn app.main:app --reload --port 8010
```

Open `http://localhost:8010/docs` and confirm `POST /api/v1/system/truncate` exists.

- [ ] **Step 3: Quick manual test (requires dev mode to be ON and superadmin token)**

```bash
# Get token first (replace with real superadmin credentials)
TOKEN=$(curl -s -X POST http://localhost:8010/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"admin@example.com","password":"yourpassword"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['access_token'])")

# Test: should fail with 403 if env mode is production
curl -s -X POST http://localhost:8010/api/v1/system/truncate \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"group":"end_to_end"}' | python3 -m json.tool
```

Expected if production mode: `{"detail": "Truncate is only allowed in development mode"}`

- [ ] **Step 4: Commit**

```bash
git add backend/app/routers/system.py
git commit -m "feat: add POST /system/truncate endpoint (superadmin + dev mode only)"
```

---

## Task 4: Frontend API + hook

**Files:**
- Modify: `frontend/src/lib/api/system.ts`
- Modify: `frontend/src/lib/hooks/useSystem.ts`

- [ ] **Step 1: Add truncate() to systemApi**

Open `frontend/src/lib/api/system.ts`. Full file after edit:

```typescript
import apiClient from './client'

export interface SystemStatus {
  maintenance_mode: boolean
  environment_mode: 'development' | 'production'
}

export type TruncateGroup =
  | 'penjualan'
  | 'stock'
  | 'penebusan_penerimaan'
  | 'expenses_penyetoran'
  | 'end_to_end'
  | 'all'

export interface TruncateResponse {
  message: string
  rows_deleted: number
}

export const systemApi = {
  /** GET /system/status — returns current maintenance_mode and environment_mode. */
  getStatus: (): Promise<SystemStatus> =>
    apiClient.get<SystemStatus>('/system/status').then((r) => r.data),

  /** PATCH /system/settings — update global system settings. */
  updateSettings: (data: {
    maintenance_mode?: boolean
    environment_mode?: 'development' | 'production'
  }): Promise<SystemStatus> =>
    apiClient.patch<SystemStatus>('/system/settings', data).then((r) => r.data),

  /** POST /system/truncate — delete all rows in an operational table group (dev mode only). */
  truncate: (group: TruncateGroup): Promise<TruncateResponse> =>
    apiClient.post<TruncateResponse>('/system/truncate', { group }).then((r) => r.data),
}
```

- [ ] **Step 2: Add useTruncate() hook**

Open `frontend/src/lib/hooks/useSystem.ts`. Full file after edit:

```typescript
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { systemApi, type TruncateGroup } from '@/lib/api/system'

/** Fetch the current system status, cached for 10 seconds. */
export function useSystemStatus() {
  return useQuery({
    queryKey: ['system', 'status'],
    queryFn: systemApi.getStatus,
    staleTime: 10_000,
  })
}

/** Toggle maintenance mode on or off. */
export function useToggleMaintenance() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: (enabled: boolean) =>
      systemApi.updateSettings({ maintenance_mode: enabled }),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['system', 'status'] })
    },
  })
}

/** Switch between 'development' and 'production' environment mode. */
export function useSetEnvMode() {
  const qc = useQueryClient()
  return useMutation({
    mutationFn: (mode: 'development' | 'production') =>
      systemApi.updateSettings({ environment_mode: mode }),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ['system', 'status'] })
    },
  })
}

/** Truncate an operational table group (dev mode only). */
export function useTruncate() {
  return useMutation({
    mutationFn: (group: TruncateGroup) => systemApi.truncate(group),
  })
}
```

- [ ] **Step 3: Commit**

```bash
git add frontend/src/lib/api/system.ts frontend/src/lib/hooks/useSystem.ts
git commit -m "feat: add systemApi.truncate() and useTruncate() hook"
```

---

## Task 5: Sidebar — add Settings entry (superadmin-only)

**Files:**
- Modify: `frontend/src/components/ui/sidebar.tsx`

- [ ] **Step 1: Add Settings to NAV_MASTER and get user from auth**

The sidebar currently renders `NAV_MASTER` unconditionally. We need to:
1. Import `useMe` hook
2. Add Settings entry
3. Conditionally render it only for superadmin

Open `frontend/src/components/ui/sidebar.tsx`. Full file after edit:

```typescript
'use client'

import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useEffect } from 'react'
import { useSPBUs } from '@/lib/hooks/useSPBUs'
import { useMe } from '@/lib/hooks/useAuth'
import { useAuthStore } from '@/stores/auth-store'
import { useSpbuStore } from '@/stores/spbu-store'

const NAV_OPERATIONAL = [
  { href: '/dashboard', nav: 'dashboard', icon: 'ti-dashboard', label: 'Dashboard' },
  { href: '/track', nav: 'track', icon: 'ti-calendar-stats', label: 'Track Your Day' },
  { href: '/penjualan', nav: 'penjualan', icon: 'ti-gas-station', label: 'Sales' },
  { href: '/stock', nav: 'stock', icon: 'ti-cylinder', label: 'Stock Adjustment' },
  { href: '/penebusan', nav: 'penebusan', icon: 'ti-truck-delivery', label: 'Fuel Purchase' },
  { href: '/penerimaan', nav: 'penerimaan', icon: 'ti-arrow-bar-down', label: 'Fuel Delivery' },
  { href: '/expenses', nav: 'expenses', icon: 'ti-receipt', label: 'Expenses' },
  { href: '/penyetoran', nav: 'penyetoran', icon: 'ti-cash', label: 'Cash Deposit' },
  { href: '/rekonsiliasi', nav: 'rekonsiliasi', icon: 'ti-chart-dots', label: 'Reconciliation' },
]

const NAV_LAPORAN = [
  { href: '/laporan', nav: 'laporan', icon: 'ti-file-analytics', label: 'Reports' },
  { href: '/analytics', nav: 'analytics', icon: 'ti-chart-bar', label: 'Analytics' },
  { href: '/anomali', nav: 'anomali', icon: 'ti-alert-triangle', label: 'Anomalies' },
]

const NAV_MASTER = [
  { href: '/spbu', nav: 'spbu', icon: 'ti-building', label: 'Stations' },
  { href: '/products', nav: 'products', icon: 'ti-tag', label: 'Products' },
  { href: '/users', nav: 'users', icon: 'ti-users', label: 'Users' },
]

export function Sidebar() {
  const pathname = usePathname()
  const { data: spbus } = useSPBUs()
  const { data: me } = useMe()
  const { activeSPBU, setActiveSPBU } = useSpbuStore()

  useEffect(() => {
    if (spbus && spbus.length > 0 && !activeSPBU) {
      setActiveSPBU(spbus[0])
    }
  }, [spbus, activeSPBU, setActiveSPBU])

  const getActiveNav = () => {
    if (pathname.startsWith('/users/roles')) return 'roles'
    if (pathname.startsWith('/users')) return 'users'
    if (pathname.startsWith('/settings')) return 'settings'
    if (pathname.startsWith('/products')) return 'products'
    if (pathname.startsWith('/spbu')) return 'spbu'
    if (pathname.startsWith('/penjualan')) return 'penjualan'
    if (pathname.startsWith('/stock')) return 'stock'
    if (pathname.startsWith('/penebusan')) return 'penebusan'
    if (pathname.startsWith('/penerimaan')) return 'penerimaan'
    if (pathname.startsWith('/expenses')) return 'expenses'
    if (pathname.startsWith('/penyetoran')) return 'penyetoran'
    if (pathname.startsWith('/rekonsiliasi')) return 'rekonsiliasi'
    if (pathname.startsWith('/laporan')) return 'laporan'
    if (pathname.startsWith('/analytics')) return 'analytics'
    if (pathname.startsWith('/anomali')) return 'anomali'
    if (pathname.startsWith('/track')) return 'track'
    if (pathname === '/dashboard') return 'dashboard'
    return ''
  }

  const activeNav = getActiveNav()

  return (
    <aside className="navbar navbar-vertical navbar-expand-lg" data-bs-theme="dark">
      <div className="container-fluid">
        <button
          className="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbar-menu"
        >
          <span className="navbar-toggler-icon"></span>
        </button>
        <h1 className="navbar-brand navbar-brand-autodark">
          <Link href="/">
            <span className="navbar-brand-text">
              ⛽ SPBU<span style={{ color: '#4da6ff' }}>Manager</span>
            </span>
          </Link>
        </h1>

        <div className="collapse navbar-collapse" id="navbar-menu">
          {spbus && spbus.length > 0 && (
            <div className="mb-2 mt-lg-2">
              <select
                className="form-select form-select-sm"
                style={{
                  background: 'rgba(255,255,255,0.08)',
                  color: 'rgba(255,255,255,0.8)',
                  borderColor: 'rgba(255,255,255,0.15)',
                }}
                value={activeSPBU?.id ?? ''}
                onChange={(e) => {
                  const spbu = spbus.find((s) => s.id === Number(e.target.value))
                  if (spbu) setActiveSPBU(spbu)
                }}
              >
                {spbus.map((s) => (
                  <option key={s.id} value={s.id}>
                    {s.nomor_pertamina} — {s.name}
                  </option>
                ))}
              </select>
            </div>
          )}

          <ul className="navbar-nav pt-lg-2">
            {/* Operasional — hanya tampil jika ada SPBU aktif */}
            {activeSPBU && (
              <>
                <li><span className="section-label">Operations</span></li>
                {NAV_OPERATIONAL.map((item) => (
                  <li key={item.nav} className="nav-item">
                    <Link href={item.href} className={`nav-link ${activeNav === item.nav ? 'active' : ''}`}>
                      <span className="nav-link-icon d-md-none d-lg-inline-block">
                        <i className={`ti ${item.icon}`}></i>
                      </span>
                      <span className="nav-link-title">{item.label}</span>
                    </Link>
                  </li>
                ))}

                <li><span className="section-label">Reports</span></li>
                {NAV_LAPORAN.map((item) => (
                  <li key={item.nav} className="nav-item">
                    <Link href={item.href} className={`nav-link ${activeNav === item.nav ? 'active' : ''}`}>
                      <span className="nav-link-icon d-md-none d-lg-inline-block">
                        <i className={`ti ${item.icon}`}></i>
                      </span>
                      <span className="nav-link-title">{item.label}</span>
                    </Link>
                  </li>
                ))}
              </>
            )}

            <li><span className="section-label">Master Data</span></li>

            {NAV_MASTER.map((item) => (
              <li key={item.nav} className="nav-item">
                <Link href={item.href} className={`nav-link ${activeNav === item.nav ? 'active' : ''}`}>
                  <span className="nav-link-icon d-md-none d-lg-inline-block">
                    <i className={`ti ${item.icon}`}></i>
                  </span>
                  <span className="nav-link-title">{item.label}</span>
                </Link>
              </li>
            ))}

            {me?.is_superadmin && (
              <li className="nav-item">
                <Link href="/settings" className={`nav-link ${activeNav === 'settings' ? 'active' : ''}`}>
                  <span className="nav-link-icon d-md-none d-lg-inline-block">
                    <i className="ti ti-settings"></i>
                  </span>
                  <span className="nav-link-title">Settings</span>
                </Link>
              </li>
            )}
          </ul>
        </div>
      </div>
    </aside>
  )
}
```

- [ ] **Step 2: Verify sidebar renders without errors**

Start the frontend dev server and open the app as a superadmin — confirm "Settings" appears at the bottom of Master Data. Log in as a non-superadmin and confirm it does not appear.

```bash
cd frontend && npm run dev
```

- [ ] **Step 3: Commit**

```bash
git add frontend/src/components/ui/sidebar.tsx
git commit -m "feat: add Settings entry to sidebar (superadmin-only)"
```

---

## Task 6: Settings page — page.tsx + System tab

**Files:**
- Create: `frontend/src/app/(dashboard)/settings/page.tsx`
- Create: `frontend/src/app/(dashboard)/settings/settings-client.tsx`

- [ ] **Step 1: Create page.tsx**

```typescript
// frontend/src/app/(dashboard)/settings/page.tsx
import { SettingsClient } from './settings-client'

export default function SettingsPage() {
  return <SettingsClient />
}
```

- [ ] **Step 2: Create settings-client.tsx with System tab**

```typescript
// frontend/src/app/(dashboard)/settings/settings-client.tsx
'use client'

import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useMe } from '@/lib/hooks/useAuth'
import {
  useSystemStatus,
  useToggleMaintenance,
  useSetEnvMode,
  useTruncate,
} from '@/lib/hooks/useSystem'
import type { TruncateGroup } from '@/lib/api/system'

type Tab = 'system' | 'dev-tools'

const TRUNCATE_GROUPS: { group: TruncateGroup; label: string; tables: string }[] = [
  {
    group: 'penjualan',
    label: 'Penjualan',
    tables: 'laporan_shift, penjualan_nozzle',
  },
  {
    group: 'stock',
    label: 'Stock Adjustment',
    tables: 'stock_adjustment, stock_adjustment_item, stock_adjustment_item_foto',
  },
  {
    group: 'penebusan_penerimaan',
    label: 'Penebusan & Penerimaan',
    tables: 'penebusan, penebusan_item, penerimaan, penerimaan_item, penerimaan_foto',
  },
  {
    group: 'expenses_penyetoran',
    label: 'Expenses & Penyetoran',
    tables: 'expenses, penyetoran',
  },
  {
    group: 'end_to_end',
    label: 'End-to-End Reconciliation',
    tables: 'end_to_end_cycle',
  },
]

// ─── Confirm Truncate Modal ───────────────────────────────────────────────────
function ConfirmTruncateModal({
  label,
  tables,
  onConfirm,
  onClose,
  isPending,
}: {
  label: string
  tables: string
  onConfirm: () => void
  onClose: () => void
  isPending: boolean
}) {
  return (
    <div className="modal modal-blur show d-block" style={{ background: 'rgba(0,0,0,0.5)' }}>
      <div className="modal-dialog modal-sm modal-dialog-centered">
        <div className="modal-content">
          <div className="modal-header">
            <h5 className="modal-title text-danger">
              <i className="ti ti-alert-triangle me-2"></i>
              Confirm Truncate
            </h5>
            <button type="button" className="btn-close" onClick={onClose} disabled={isPending}></button>
          </div>
          <div className="modal-body">
            <p className="mb-1">
              This will permanently delete <strong>all rows</strong> from:
            </p>
            <p className="fw-bold mb-1">{label}</p>
            <p className="text-muted small mb-0">{tables}</p>
            <div className="alert alert-danger mt-3 mb-0 py-2">
              <i className="ti ti-skull me-1"></i>
              This action cannot be undone.
            </div>
          </div>
          <div className="modal-footer">
            <button className="btn btn-ghost-secondary" onClick={onClose} disabled={isPending}>
              Cancel
            </button>
            <button className="btn btn-danger" onClick={onConfirm} disabled={isPending}>
              {isPending ? (
                <span className="spinner-border spinner-border-sm me-1"></span>
              ) : (
                <i className="ti ti-trash me-1"></i>
              )}
              Yes, truncate
            </button>
          </div>
        </div>
      </div>
    </div>
  )
}

// ─── Main Component ───────────────────────────────────────────────────────────
export function SettingsClient() {
  const router = useRouter()
  const { data: me, isLoading: meLoading } = useMe()
  const { data: systemStatus } = useSystemStatus()
  const toggleMaintenance = useToggleMaintenance()
  const setEnvMode = useSetEnvMode()
  const truncate = useTruncate()

  const [activeTab, setActiveTab] = useState<Tab>('system')
  const [successMsg, setSuccessMsg] = useState<string | null>(null)
  const [confirmModal, setConfirmModal] = useState<{
    group: TruncateGroup
    label: string
    tables: string
  } | null>(null)

  const isDevMode = systemStatus?.environment_mode === 'development'
  const isMaintenance = systemStatus?.maintenance_mode ?? false

  // Redirect non-superadmin
  useEffect(() => {
    if (!meLoading && me && !me.is_superadmin) {
      router.replace('/')
    }
  }, [me, meLoading, router])

  // If on dev-tools tab and env switches to production, go back to system tab
  useEffect(() => {
    if (activeTab === 'dev-tools' && !isDevMode) {
      setActiveTab('system')
    }
  }, [isDevMode, activeTab])

  if (meLoading || !me) return null
  if (!me.is_superadmin) return null

  const handleTruncateConfirm = () => {
    if (!confirmModal) return
    truncate.mutate(confirmModal.group, {
      onSuccess: (data) => {
        setSuccessMsg(`${confirmModal!.label} truncated — ${data.rows_deleted} rows deleted.`)
        setConfirmModal(null)
        setTimeout(() => setSuccessMsg(null), 5000)
      },
      onError: () => {
        setConfirmModal(null)
      },
    })
  }

  return (
    <div className="page-header d-print-none">
      <div className="container-xl">
        <div className="row g-2 align-items-center mb-3">
          <div className="col">
            <h2 className="page-title">Settings</h2>
            <div className="text-muted mt-1">System configuration and developer tools</div>
          </div>
        </div>

        {/* Tabs */}
        <div className="card">
          <div className="card-header">
            <ul className="nav nav-tabs card-header-tabs">
              <li className="nav-item">
                <button
                  className={`nav-link ${activeTab === 'system' ? 'active' : ''}`}
                  onClick={() => setActiveTab('system')}
                >
                  <i className="ti ti-adjustments me-1"></i>
                  System
                </button>
              </li>
              {isDevMode && (
                <li className="nav-item">
                  <button
                    className={`nav-link ${activeTab === 'dev-tools' ? 'active' : ''}`}
                    onClick={() => setActiveTab('dev-tools')}
                  >
                    <i className="ti ti-code me-1"></i>
                    Developer Tools
                    <span className="badge bg-red ms-2" style={{ fontSize: '0.6rem' }}>DEV</span>
                  </button>
                </li>
              )}
            </ul>
          </div>

          <div className="card-body">
            {/* ── System Tab ── */}
            {activeTab === 'system' && (
              <div>
                <h3 className="mb-3">System Configuration</h3>

                {/* Maintenance Mode */}
                <div className="mb-4 pb-4" style={{ borderBottom: '1px solid var(--tblr-border-color)' }}>
                  <div className="row align-items-center">
                    <div className="col">
                      <div className="fw-bold">Maintenance Mode</div>
                      <div className="text-muted small">
                        When ON, all non-superadmin users are blocked from accessing the app.
                      </div>
                    </div>
                    <div className="col-auto">
                      <label className="form-check form-switch mb-0">
                        <input
                          className="form-check-input"
                          type="checkbox"
                          checked={isMaintenance}
                          disabled={toggleMaintenance.isPending}
                          onChange={(e) => toggleMaintenance.mutate(e.target.checked)}
                        />
                        <span className="form-check-label fw-bold" style={{ color: isMaintenance ? '#d97706' : undefined }}>
                          {isMaintenance ? 'ON' : 'OFF'}
                        </span>
                      </label>
                    </div>
                  </div>
                </div>

                {/* Environment Mode */}
                <div className="row align-items-center">
                  <div className="col">
                    <div className="fw-bold">Environment Mode</div>
                    <div className="text-muted small">
                      <strong>Development:</strong> enables hard delete and Developer Tools tab.{' '}
                      <strong>Production:</strong> soft delete only, dev tools hidden.
                    </div>
                  </div>
                  <div className="col-auto">
                    <div className="btn-group">
                      <button
                        className={`btn btn-sm ${isDevMode ? 'btn-danger' : 'btn-ghost-secondary'}`}
                        disabled={setEnvMode.isPending || isDevMode}
                        onClick={() => setEnvMode.mutate('development')}
                      >
                        <i className="ti ti-code me-1"></i>Development
                      </button>
                      <button
                        className={`btn btn-sm ${!isDevMode ? 'btn-success' : 'btn-ghost-secondary'}`}
                        disabled={setEnvMode.isPending || !isDevMode}
                        onClick={() => setEnvMode.mutate('production')}
                      >
                        <i className="ti ti-building-store me-1"></i>Production
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            )}

            {/* ── Developer Tools Tab ── */}
            {activeTab === 'dev-tools' && isDevMode && (
              <div>
                <div className="alert alert-danger d-flex align-items-center mb-4">
                  <i className="ti ti-alert-triangle me-2 fs-3"></i>
                  <div>
                    <strong>DANGER ZONE — Development Mode Only</strong>
                    <div className="small">Data yang di-truncate tidak bisa di-recover.</div>
                  </div>
                </div>

                {successMsg && (
                  <div className="alert alert-success d-flex align-items-center mb-3">
                    <i className="ti ti-check me-2"></i>
                    {successMsg}
                  </div>
                )}

                {/* Reset All button */}
                <div className="mb-4">
                  <button
                    className="btn btn-danger"
                    onClick={() =>
                      setConfirmModal({
                        group: 'all',
                        label: 'All Operational Tables',
                        tables: TRUNCATE_GROUPS.map((g) => g.tables).join(', '),
                      })
                    }
                    disabled={truncate.isPending}
                  >
                    <i className="ti ti-trash me-2"></i>
                    Reset All Operations
                  </button>
                </div>

                {/* Per-group table */}
                <div className="table-responsive">
                  <table className="table table-vcenter card-table">
                    <thead>
                      <tr>
                        <th>Group</th>
                        <th>Tables</th>
                        <th style={{ width: 120 }}></th>
                      </tr>
                    </thead>
                    <tbody>
                      {TRUNCATE_GROUPS.map((item) => (
                        <tr key={item.group}>
                          <td className="fw-bold">{item.label}</td>
                          <td className="text-muted small">{item.tables}</td>
                          <td>
                            <button
                              className="btn btn-sm btn-outline-danger"
                              disabled={truncate.isPending}
                              onClick={() => setConfirmModal(item)}
                            >
                              <i className="ti ti-trash me-1"></i>
                              Truncate
                            </button>
                          </td>
                        </tr>
                      ))}
                    </tbody>
                  </table>
                </div>
              </div>
            )}
          </div>
        </div>
      </div>

      {/* Confirm Modal */}
      {confirmModal && (
        <ConfirmTruncateModal
          label={confirmModal.label}
          tables={confirmModal.tables}
          onConfirm={handleTruncateConfirm}
          onClose={() => setConfirmModal(null)}
          isPending={truncate.isPending}
        />
      )}
    </div>
  )
}
```

- [ ] **Step 3: Verify page loads**

With dev server running, navigate to `http://localhost:3000/settings` as superadmin. Confirm:
- Page loads with "System" tab active
- Maintenance Mode toggle reflects current state and toggles correctly
- Environment Mode shows correct active button
- When env mode = development, "Developer Tools" tab is visible
- When env mode = production, "Developer Tools" tab disappears
- Navigating to `/settings` as non-superadmin redirects to `/`

- [ ] **Step 4: Verify truncate flow**

1. Switch env mode to `development` via System tab
2. Click Developer Tools tab — it should appear
3. Click "Truncate" on End-to-End group
4. Modal appears with group name and table list
5. Confirm — success alert shows rows deleted count
6. Switch env mode back to `production` — Dev Tools tab disappears, active tab shifts to System

- [ ] **Step 5: Commit**

```bash
git add frontend/src/app/(dashboard)/settings/
git commit -m "feat: add Settings page with System and Developer Tools tabs"
```

---

## Self-Review Checklist

- [x] **Spec coverage:** All sections covered — URL/nav (Task 5), access control (Task 6 useEffect redirect + sidebar conditional), System tab (Task 6), Dev Tools tab (Task 6), truncate endpoint (Tasks 1-3), API+hook (Task 4), FK order (Task 2 `_TRUNCATE_ORDER`), modal confirmation (Task 6 `ConfirmTruncateModal`), success feedback (Task 6 `successMsg`), env-switch redirect from dev-tools tab (Task 6 `useEffect`).
- [x] **Placeholder scan:** No TBD/TODO in any task. All code blocks are complete.
- [x] **Type consistency:** `TruncateGroup` defined in `system.ts` Task 4 Step 1, imported in `useSystem.ts` Task 4 Step 2, and used in `settings-client.tsx` Task 6 Step 2. `useTruncate` defined in Task 4, imported in Task 6. `TRUNCATE_GROUPS` items use the same `TruncateGroup` type.
- [x] **Double protection:** Backend rejects truncate if env != development (Task 3). Frontend hides Dev Tools tab in production (Task 6).
- [x] **useAuthStore unused:** Sidebar now uses `useMe()` directly instead of `useAuthStore` for superadmin check — this is consistent with how topbar checks `user.is_superadmin`.
