import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { api, errorMessage } from '../services/apiClient.ts'; import { useAuth } from '../context/AuthContext.tsx'; import { useFarm } from '../context/FarmContext.tsx'; import type { CitySite, User, UserStatus } from '../types/api.ts'; import { Banner, PageHeader, Spinner } from './ui/Feedback.tsx'; const inputClass = 'border border-gray-300 rounded-md px-3 py-2 text-sm w-full focus:ring-red-500 focus:border-red-500'; const ROLE_LABELS: Partial> = { gm: 'GM', gm_admin: 'GM Admin', buh: 'BUH', bu_admin: 'BU Admin', account_admin: 'Account Admin', }; function slugify(value: string): string { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 64); } function isGmRole(status?: UserStatus | string | null): boolean { return status === 'gm' || status === 'gm_admin'; } function isBuhRole(status?: UserStatus | string | null): boolean { return status === 'buh' || status === 'bu_admin'; } const BuhViewList: React.FC = () => { const { user } = useAuth(); const isPrimaryBuh = user?.status === 'buh'; const [gms, setGms] = useState([]); const [pending, setPending] = useState([]); const [citySites, setCitySites] = useState([]); const [loading, setLoading] = useState(true); const [busyId, setBusyId] = useState(null); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const reload = useCallback(async () => { setLoading(true); setError(null); try { const nextUsers = await api.users.list(); const nextSites = await api.citySites.list(); const nextPending = isPrimaryBuh ? await api.users.listPendingManaged() : []; setGms(nextUsers.filter((u) => u.status === 'gm')); setPending(nextPending); setCitySites(nextSites); } catch (err) { setError(errorMessage(err)); } finally { setLoading(false); } }, [isPrimaryBuh]); useEffect(() => { void reload(); }, [reload]); const sitesByGm = useMemo(() => { const map = new Map(); for (const site of citySites) { if (site.registered_by == null) continue; const rows = map.get(site.registered_by) ?? []; rows.push(site); map.set(site.registered_by, rows); } return map; }, [citySites]); const decide = async (gmId: number, decision: 'approve' | 'reject') => { setBusyId(gmId); setError(null); try { if (decision === 'approve') { await api.users.approveManagedBy(gmId); setNotice('GM dikonfirmasi di bawah pengelolaan Anda.'); } else { await api.users.rejectManagedBy(gmId); setNotice('Permintaan GM ditolak.'); } await reload(); } catch (err) { setError(errorMessage(err)); } finally { setBusyId(null); } }; return (
{error && (
{error}
)} {notice && (
{notice}
)} {isPrimaryBuh && (

Permintaan GM

GM memilih Anda sebagai BUH. Konfirmasi jika mereka memang di bawah pengawasan Anda.

{loading ? ( ) : pending.length === 0 ? (

Tidak ada permintaan menunggu.

) : (
    {pending.map((gm) => (
  • {gm.user_name}

    {ROLE_LABELS[gm.status] ?? gm.status}

  • ))}
)}
)}

GM yang dikelola

Sudah dikonfirmasi (akun GM saja). Site di daftar tampilan mereka otomatis masuk rollup Anda.

{loading ? ( ) : gms.length === 0 ? (

Belum ada GM yang terkonfirmasi di bawah Anda.

) : (
    {gms.map((gm) => { const sites = sitesByGm.get(gm.id) ?? []; return (
  • {gm.user_name}

    {ROLE_LABELS[gm.status] ?? gm.status} · {sites.length} site

    {sites.length === 0 ? (

    Belum ada site di daftar tampilan GM ini.

    ) : (
      {sites.map((site) => (
    • {site.name}

      {site.code} {site.is_active ? '' : ' · nonaktif'}

    • ))}
    )}
  • ); })}
)}
); }; const GmViewList: React.FC = () => { const { user, refreshUser } = useAuth(); const { reload: reloadFarm } = useFarm(); const [citySites, setCitySites] = useState([]); const [buhs, setBuhs] = useState([]); const [selectedBuhId, setSelectedBuhId] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [buhBusy, setBuhBusy] = useState(false); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [name, setName] = useState(''); const [code, setCode] = useState(''); const [codeTouched, setCodeTouched] = useState(false); const [apiBaseUrl, setApiBaseUrl] = useState(''); const [apiKey, setApiKey] = useState(''); const flash = (msg: string) => { setNotice(msg); setError(null); }; const reload = useCallback(async () => { setLoading(true); setError(null); try { const sitesPromise = api.citySites.list(); const mePromise = refreshUser(); const buhsPromise = user?.status === 'gm' ? api.users.listBuhs() : Promise.resolve([] as User[]); const [sites, me, nextBuhs] = await Promise.all([sitesPromise, mePromise, buhsPromise]); setCitySites(sites); setBuhs(nextBuhs); if (me?.managed_by_pending) { setSelectedBuhId(me.managed_by_pending); } else if (me?.managed_by) { setSelectedBuhId(me.managed_by); } } catch (err) { setError(errorMessage(err)); } finally { setLoading(false); } }, [refreshUser, user?.status]); useEffect(() => { void reload(); }, [reload]); const resetForm = () => { setName(''); setCode(''); setCodeTouched(false); setApiBaseUrl(''); setApiKey(''); }; const wrap = async (fn: () => Promise) => { setSaving(true); setError(null); try { await fn(); await reload(); await reloadFarm(); } catch (err) { setError(errorMessage(err)); } finally { setSaving(false); } }; const submitBuhRequest = async () => { if (selectedBuhId === '') return; setBuhBusy(true); setError(null); try { await api.users.requestManagedBy(selectedBuhId); await refreshUser(); flash('Permintaan dikirim. Menunggu konfirmasi BUH.'); } catch (err) { setError(errorMessage(err)); } finally { setBuhBusy(false); } }; const cancelBuhRequest = async () => { setBuhBusy(true); setError(null); try { await api.users.cancelManagedByRequest(); await refreshUser(); flash('Permintaan dibatalkan.'); } catch (err) { setError(errorMessage(err)); } finally { setBuhBusy(false); } }; return (
{error && (
{error}
)} {notice && (
{notice}
)} {user?.status === 'gm' && (

BUH pengelola

Pilih BUH yang membawahi Anda. Mereka harus mengonfirmasi sebelum Anda masuk ke rollup mereka.

{loading ? ( ) : (
{user?.managed_by_name && (

Saat ini dikonfirmasi di bawah:{' '} {user.managed_by_name}

)} {user?.managed_by_pending_name && (

Menunggu konfirmasi dari {user.managed_by_pending_name}

)}
{user?.managed_by_pending != null && ( )}
)}
)}

Tambah site

Daftarkan URL API dasbor kota agar data site muncul di daftar tampilan Anda.

{ e.preventDefault(); const resolvedCode = code || slugify(name); if (!resolvedCode) return; void wrap(async () => { const created = await api.citySites.create({ code: resolvedCode, name: name.trim(), api_base_url: apiBaseUrl.trim(), ...(apiKey.trim() ? { api_key: apiKey.trim() } : {}), is_active: true, }); resetForm(); flash(`Site ${created.name} ditambahkan ke daftar tampilan.`); }); }} >

Daftar tampilan

Site yang terdaftar untuk akun GM / GM Admin Anda.

{loading ? ( ) : citySites.length === 0 ? (

Belum ada site di daftar tampilan.

) : (
    {citySites.map((site) => (
  • {site.name}

    {site.code}

    {site.api_base_url}

    {site.is_active ? 'Aktif' : 'Nonaktif'} {site.has_api_key ? ' · API key tersimpan' : ''}

  • ))}
)}
); }; const SettingsPage: React.FC = () => { const { user } = useAuth(); if (isGmRole(user?.status)) { return ; } if (isBuhRole(user?.status)) { return ; } return (
Pengaturan daftar tampilan tersedia untuk GM, GM Admin, BUH, dan BU Admin.
); }; export default SettingsPage;