first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,106 @@
|
||||
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;
|
||||
Reference in new issue
Block a user