import React, { useEffect, useState } from 'react'; import { api, errorMessage } from '../services/apiClient.ts'; import { useAuth } from '../context/AuthContext.tsx'; import { useFarm } from '../context/FarmContext.tsx'; import type { Cycle, FeedInButtonUrl, PusatRegistration } from '../types/api.ts'; import { Banner, ConfirmDialog, PageHeader } from './ui/Feedback.tsx'; import { formatDateId, resolveCycleStatus, todayIso } from '../utils/format.ts'; const inputClass = 'border border-gray-300 rounded-md px-3 py-2 text-sm w-full focus:ring-red-500 focus:border-red-500'; function slugify(value: string): string { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 64); } type PusatConfirm = | { kind: 'register'; code: string; siteName: string } | { kind: 'change-code'; code: string; previousCode: string }; const defaultCycleForm = () => ({ startDate: todayIso(), docInCount: 20000, chickInWeight: 35, }); const cycleToForm = (cycle: Cycle) => ({ startDate: cycle.start_date, docInCount: cycle.doc_in_count, chickInWeight: cycle.chick_in_weight, }); const emptyFeedInButtonUrl = (): FeedInButtonUrl => ({ label: '', url: '' }); const SettingsPage: React.FC = () => { const { user } = useAuth(); const { sites, kandangs, flocks, cycles, selectedSiteId, selectedKandangId, reload, setSelectedSiteId, setSelectedKandangId, setSelectedCycleId, } = useFarm(); const [error, setError] = useState(null); const [notice, setNotice] = useState(null); const [saving, setSaving] = useState(false); const [siteName, setSiteName] = useState(''); const [kandangName, setKandangName] = useState(''); const [feedInButtonUrls, setFeedInButtonUrls] = useState([ emptyFeedInButtonUrl(), ]); const [flockName, setFlockName] = useState(''); const [editingCycleId, setEditingCycleId] = useState(null); const [startDate, setStartDate] = useState(defaultCycleForm().startDate); const [docInCount, setDocInCount] = useState(defaultCycleForm().docInCount); const [chickInWeight, setChickInWeight] = useState(defaultCycleForm().chickInWeight); const [registerCode, setRegisterCode] = useState(''); const [pusatRegistration, setPusatRegistration] = useState(null); const [pusatConfirm, setPusatConfirm] = useState(null); const resetCycleForm = () => { const defaults = defaultCycleForm(); setEditingCycleId(null); setStartDate(defaults.startDate); setDocInCount(defaults.docInCount); setChickInWeight(defaults.chickInWeight); }; const beginCycleEdit = (cycle: Cycle) => { const form = cycleToForm(cycle); setEditingCycleId(cycle.id); setStartDate(form.startDate); setDocInCount(form.docInCount); setChickInWeight(form.chickInWeight); setSelectedCycleId(cycle.id); }; const buildCyclePayload = () => ({ kandang: selectedKandangId!, total_days: 1, start_date: startDate, chick_in_weight: chickInWeight, doc_in_count: docInCount, }); const flash = (msg: string) => { setNotice(msg); setError(null); }; const isSuperAdmin = user?.status === 'superadmin'; const canRequestClose = user?.status === 'active'; const selectedSite = sites.find((s) => s.id === selectedSiteId) ?? null; useEffect(() => { if (!canRequestClose || selectedSiteId == null) { setPusatRegistration(null); return; } let cancelled = false; void (async () => { try { const rows = await api.pusat.registration({ site_id: selectedSiteId }); if (!cancelled) { setPusatRegistration(rows[0] ?? null); if (rows[0]?.code) setRegisterCode(rows[0].code); } } catch { if (!cancelled) setPusatRegistration(null); } })(); return () => { cancelled = true; }; }, [canRequestClose, selectedSiteId]); const statusLabel = (cycle: Cycle) => { const status = resolveCycleStatus(cycle); if (status === 'pending_close') return 'Menunggu persetujuan GM'; if (status === 'closed') return 'Selesai'; return 'Aktif'; }; const selectedKandang = kandangs.find((k) => k.id === selectedKandangId) ?? null; const editingCycle = editingCycleId ? (cycles.find((c) => c.id === editingCycleId) ?? null) : null; React.useEffect(() => { const urls = selectedKandang?.feed_in_button_urls ?? []; setFeedInButtonUrls( urls.length > 0 ? urls.map((entry) => ({ ...entry })) : [emptyFeedInButtonUrl()] ); }, [selectedKandang?.id, selectedKandang?.feed_in_button_urls]); const wrap = async (fn: () => Promise) => { setSaving(true); setError(null); setNotice(null); try { await fn(); await reload(); } catch (err) { setNotice(null); setError(errorMessage(err)); } finally { setSaving(false); } }; return (
{error && (
{error}
)} {notice && (
{notice}
)}

Sites

{isSuperAdmin ? (
{ e.preventDefault(); if (!user) return; void wrap(async () => { const created = await api.sites.create({ site_name: siteName, user: user.id }); setSiteName(''); setSelectedSiteId(created.id); flash(`Site ${created.site_name} dibuat.`); }); }} > setSiteName(e.target.value)} required />
) : (

Hanya admin yang dapat membuat site.

)}
    {sites.map((site) => (
  • {isSuperAdmin && ( )}
  • ))}
{canRequestClose && (

Daftar ke pusat

Pilih site di daftar kiri, lalu masukkan kode. GM di dasbor pusat akan memakai kode yang sama untuk menambahkan site ke daftar tampilan mereka.

{!selectedSite ? (

Pilih site terlebih dahulu.

) : (

Site: {selectedSite.site_name}

{pusatRegistration && (

Sudah terdaftar — kode {pusatRegistration.code} {pusatRegistration.registered_at ? ` · ${formatDateId(pusatRegistration.registered_at.slice(0, 10))}` : ''}

)}
{pusatRegistration && ( )}
{pusatRegistration && (

Daftar ke pusat terkunci karena site sudah terdaftar. Gunakan Ubah kode untuk mengganti kode.

)}
)}
)}

Kandang

{isSuperAdmin ? (
{ e.preventDefault(); if (!selectedSiteId) return; void wrap(async () => { const created = await api.kandangs.create({ kandang_name: kandangName, site: selectedSiteId, }); setKandangName(''); setSelectedKandangId(created.id); flash(`Kandang ${created.kandang_name} dibuat.`); }); }} > setKandangName(e.target.value)} required />
) : (

Hanya admin yang dapat membuat kandang.

)}
    {kandangs.map((kandang) => (
  • {isSuperAdmin && ( )}
  • ))}
{selectedKandang && isSuperAdmin && (
{ e.preventDefault(); void wrap(async () => { const payload = feedInButtonUrls .map((entry) => ({ label: entry.label.trim(), url: entry.url.trim(), })) .filter((entry) => entry.url); await api.kandangs.update(selectedKandang.id, { feed_in_button_urls: payload, }); flash(`URL Feed In/Out Button untuk ${selectedKandang.kandang_name} disimpan.`); }); }} >

URL Feed In/Out Button

Tambahkan satu atau lebih alamat hitung feed in untuk kandang ini.

{feedInButtonUrls.map((entry, index) => (
{feedInButtonUrls.length > 1 && ( )}
))}
)}

Lantai

Lantai tetap untuk kandang ini (dipakai Panel IoT).

{isSuperAdmin ? (
{ e.preventDefault(); if (!selectedKandangId) return; void wrap(async () => { const created = await api.flocks.create({ flock_name: flockName, kandang: selectedKandangId, }); setFlockName(''); flash(`Lantai ${created.flock_name} dibuat.`); }); }} > setFlockName(e.target.value)} required />
) : (

Hanya admin yang dapat membuat lantai.

)} {!selectedKandangId ? (

Pilih kandang terlebih dahulu.

) : (
    {flocks.map((flock) => { const webhookUrl = `${window.location.origin}/api/v1/panel-iot/push/${flock.id}/`; return (
  • {flock.flock_name} {isSuperAdmin && ( )}
    {webhookUrl}
  • ); })}
)}

Siklus

{ e.preventDefault(); if (!selectedKandangId) return; void wrap(async () => { const payload = buildCyclePayload(); if (editingCycleId) { await api.cycles.update(editingCycleId, payload); flash('Siklus diperbarui.'); resetCycleForm(); } else { await api.cycles.create({ ...payload, current_day: 1 }); flash('Siklus dibuat.'); resetCycleForm(); } }); }} > {editingCycle && (

Mengedit siklus mulai {formatDateId(startDate)} {editingCycle.end_date ? ` (selesai ${formatDateId(editingCycle.end_date)}, hanya via GM)` : ' — tanggal selesai ditetapkan setelah persetujuan GM'}

)}

Tanggal selesai tidak diisi di sini. Ajukan tutup siklus dari Dashboard setelah produksi selesai; GM yang menyetujui end date.

{editingCycleId && ( )}
    {cycles.map((cycle) => { const status = resolveCycleStatus(cycle); return (
  • ); })}
Kode akan diganti dari {pusatConfirm.previousCode} menjadi{' '} {pusatConfirm.code}. GM yang sudah memakai kode lama perlu menyesuaikan.

) : pusatConfirm ? (

Site {pusatConfirm.siteName} akan didaftarkan ke pusat dengan kode{' '} {pusatConfirm.code}. Setelah terdaftar, tombol Daftar terkunci.

) : null } confirmLabel={pusatConfirm?.kind === 'change-code' ? 'Ya, ubah kode' : 'Ya, daftar'} busy={saving} onCancel={() => { if (!saving) setPusatConfirm(null); }} onConfirm={() => { if (!pusatConfirm || !selectedSiteId || saving) return; const pending = pusatConfirm; void wrap(async () => { if (pending.kind === 'register') { const reg = await api.pusat.register({ code: pending.code, site_id: selectedSiteId, }); setPusatRegistration(reg); setRegisterCode(reg.code); flash(`Site ${reg.site_name} terdaftar ke pusat (kode ${reg.code}).`); } else { const reg = await api.pusat.changeCode({ code: pending.code, site_id: selectedSiteId, }); setPusatRegistration(reg); setRegisterCode(reg.code); flash(`Kode pusat diubah menjadi ${reg.code}.`); } setPusatConfirm(null); }); }} />
); }; export default SettingsPage;