Files
dashboard-cpsp/components/dashboard/DashboardCloseCycleControls.tsx
T
2026-10-01 13:23:45 +07:00

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;