Files
dashboard-cpsp-executive/components/PendingCloseQueue.tsx
T
2026-09-08 15:21:23 +07:00

107 lines
3.8 KiB
TypeScript

import React, { useMemo, useState } from 'react';
import { api, errorMessage } from '../services/apiClient.ts';
import { useAuth } from '../context/AuthContext.tsx';
import { useFarm } from '../context/FarmContext.tsx';
import { Banner } from './ui/Feedback.tsx';
import { formatDateId, resolveCycleStatus } from '../utils/format.ts';
/** GM queue: approve/reject pending cycle closes from city sites. */
const PendingCloseQueue: React.FC = () => {
const { user } = useAuth();
const { cycles, sites, kandangs, reload } = useFarm();
const [error, setError] = useState<string | null>(null);
const [notice, setNotice] = useState<string | null>(null);
const [busyId, setBusyId] = useState<number | null>(null);
const isGm = user?.status === 'gm';
const pending = useMemo(
() => cycles.filter((c) => resolveCycleStatus(c) === 'pending_close'),
[cycles]
);
if (pending.length === 0) return null;
const siteName = (kandangId: number) => {
const kandang = kandangs.find((k) => k.id === kandangId);
const site = sites.find((s) => s.id === kandang?.site);
return site?.site_name ?? '—';
};
const act = async (id: number, decision: 'approve' | 'reject') => {
setBusyId(id);
setError(null);
try {
const result =
decision === 'approve'
? await api.cycles.approveClose(id)
: await api.cycles.rejectClose(id);
const siteError =
result && typeof result === 'object' && 'site_error' in result
? (result as { site_error?: string }).site_error
: undefined;
setNotice(
siteError
? `Keputusan tersimpan di pusat. ${siteError}`
: decision === 'approve'
? 'Siklus ditutup.'
: 'Permintaan ditolak.'
);
await reload();
} catch (err) {
setError(errorMessage(err));
} finally {
setBusyId(null);
}
};
return (
<section className="bg-white rounded-xl shadow-sm p-5 mb-6">
<h2 className="font-semibold text-gray-800 mb-1">Permintaan tutup siklus</h2>
<p className="text-xs text-gray-500 mb-3">Menunggu persetujuan GM</p>
{error && <Banner kind="error">{error}</Banner>}
{notice && <Banner kind="success">{notice}</Banner>}
<ul className="divide-y">
{pending.map((cycle) => (
<li key={cycle.id} className="py-3 flex flex-wrap items-center justify-between gap-3">
<div className="text-sm text-gray-800">
<p className="font-medium">
{siteName(cycle.kandang)} · {cycle.kandang_name}
</p>
<p className="text-xs text-gray-500">
Mulai {formatDateId(cycle.start_date)}
{cycle.proposed_end_date
? ` · usulan tutup ${formatDateId(cycle.proposed_end_date)}`
: ''}
</p>
</div>
{isGm ? (
<div className="flex gap-2">
<button
type="button"
disabled={busyId === cycle.id}
className="bg-red-600 text-white px-3 py-1.5 rounded-lg text-xs font-semibold disabled:opacity-50"
onClick={() => void act(cycle.id, 'approve')}
>
Setujui
</button>
<button
type="button"
disabled={busyId === cycle.id}
className="border border-gray-300 text-gray-700 px-3 py-1.5 rounded-lg text-xs font-semibold hover:bg-gray-50 disabled:opacity-50"
onClick={() => void act(cycle.id, 'reject')}
>
Tolak
</button>
</div>
) : (
<span className="text-xs text-gray-400">Hanya GM yang dapat menyetujui</span>
)}
</li>
))}
</ul>
</section>
);
};
export default PendingCloseQueue;