160 lines
5.4 KiB
TypeScript
160 lines
5.4 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { api, errorMessage } from '../../services/apiClient.ts';
|
|
import { useAuth } from '../../context/AuthContext.tsx';
|
|
import { useFarm } from '../../context/FarmContext.tsx';
|
|
import { AlertDialog, Banner, ConfirmDialog } from '../ui/Feedback.tsx';
|
|
import { formatDateId, resolveCycleStatus, todayIso } from '../../utils/format.ts';
|
|
|
|
type ConfirmKind = 'request_close' | 'cancel_request';
|
|
|
|
const DashboardCloseCycleControls: React.FC = () => {
|
|
const { user } = useAuth();
|
|
const { selectedCycle, reload } = useFarm();
|
|
const [proposedCloseDate, setProposedCloseDate] = useState(todayIso());
|
|
const [confirmKind, setConfirmKind] = useState<ConfirmKind | null>(null);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [notice, setNotice] = useState<string | null>(null);
|
|
|
|
const canRequestClose = user?.status === 'active';
|
|
const status = selectedCycle ? resolveCycleStatus(selectedCycle) : null;
|
|
const showControls = Boolean(canRequestClose && selectedCycle && status !== 'closed');
|
|
|
|
const run = async (action: () => Promise<void>, successMessage: string) => {
|
|
if (!selectedCycle) return;
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
await action();
|
|
await reload();
|
|
setNotice(successMessage);
|
|
setConfirmKind(null);
|
|
} catch (err) {
|
|
setConfirmKind(null);
|
|
setError(errorMessage(err));
|
|
void reload();
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
};
|
|
|
|
if (!showControls && !error) return null;
|
|
|
|
return (
|
|
<div className="flex flex-col items-end gap-1">
|
|
{showControls && notice && <Banner kind="success">{notice}</Banner>}
|
|
{showControls && status === 'active' && selectedCycle && (
|
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
<label className="flex items-center gap-1 text-xs text-gray-600">
|
|
<span className="sr-only">Tanggal usulan tutup</span>
|
|
<input
|
|
type="date"
|
|
className="border border-gray-300 rounded-lg px-2 py-2 text-sm"
|
|
min={selectedCycle.start_date}
|
|
value={proposedCloseDate}
|
|
onChange={(e) => setProposedCloseDate(e.target.value)}
|
|
disabled={busy}
|
|
/>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="rounded-lg bg-red-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-700 disabled:opacity-50"
|
|
disabled={busy || !proposedCloseDate}
|
|
onClick={() => {
|
|
setNotice(null);
|
|
setError(null);
|
|
setConfirmKind('request_close');
|
|
}}
|
|
>
|
|
Ajukan tutup
|
|
</button>
|
|
</div>
|
|
)}
|
|
{showControls && status === 'pending_close' && selectedCycle && (
|
|
<div className="flex flex-col items-end gap-1">
|
|
{selectedCycle.proposed_end_date && (
|
|
<p className="text-xs text-amber-700">
|
|
Menunggu GM · usulan {formatDateId(selectedCycle.proposed_end_date)}
|
|
</p>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="rounded-lg border border-amber-600 px-4 py-2 text-sm font-semibold text-amber-800 hover:bg-amber-50 disabled:opacity-50"
|
|
disabled={busy}
|
|
onClick={() => {
|
|
setNotice(null);
|
|
setError(null);
|
|
setConfirmKind('cancel_request');
|
|
}}
|
|
>
|
|
Batalkan permintaan
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<ConfirmDialog
|
|
open={confirmKind === 'request_close'}
|
|
title="Ajukan tutup siklus?"
|
|
message={
|
|
<p>
|
|
Permintaan tutup akan dikirim ke pusat dengan tanggal usulan{' '}
|
|
<strong>{formatDateId(proposedCloseDate)}</strong>. GM harus menyetujui sebelum siklus
|
|
ditutup. Anda masih bisa membatalkan selama status menunggu GM.
|
|
</p>
|
|
}
|
|
confirmLabel="Ya, ajukan tutup"
|
|
busy={busy}
|
|
onCancel={() => {
|
|
if (!busy) setConfirmKind(null);
|
|
}}
|
|
onConfirm={() => {
|
|
if (busy || !selectedCycle) return;
|
|
void run(
|
|
() =>
|
|
api.cycles
|
|
.requestClose(selectedCycle.id, {
|
|
proposed_end_date: proposedCloseDate,
|
|
})
|
|
.then(() => undefined),
|
|
'Permintaan tutup dikirim ke pusat.'
|
|
);
|
|
}}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
open={confirmKind === 'cancel_request'}
|
|
title="Batalkan permintaan tutup?"
|
|
message={
|
|
<p>
|
|
Permintaan tutup ke pusat akan dibatalkan. Siklus tetap aktif dan dapat diajukan tutup
|
|
lagi nanti.
|
|
</p>
|
|
}
|
|
confirmLabel="Ya, batalkan"
|
|
busy={busy}
|
|
onCancel={() => {
|
|
if (!busy) setConfirmKind(null);
|
|
}}
|
|
onConfirm={() => {
|
|
if (busy || !selectedCycle) return;
|
|
void run(
|
|
() => api.cycles.cancelCloseRequest(selectedCycle.id).then(() => undefined),
|
|
'Permintaan tutup dibatalkan.'
|
|
);
|
|
}}
|
|
/>
|
|
|
|
<AlertDialog
|
|
open={error != null}
|
|
title="Gagal mengajukan tutup"
|
|
kind="error"
|
|
message={<p className="whitespace-pre-wrap break-words">{error}</p>}
|
|
dismissLabel="Tutup"
|
|
onDismiss={() => setError(null)}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default DashboardCloseCycleControls;
|