import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { fetchDrawerMovements, recordDrawerMovement, reviewDrawerMovement, type DrawerMovement, type MovementStatus, } from "./api.js"; import { formatMoney, formatRelativeDateTime } from "./lib/format.js"; import { Panel } from "./ui/Panel.js"; // Drawer cash movements. Operators RECORD receipts (Mandat Arkëtimi / cash_in) and // disbursements (Mandat Pagese / cash_out) freely; admins REVIEW them after the fact // (authorize/deny — a flag, never a cash reversal). A denial is a judgment about the // operator, settled outside the app: the drawer balance is untouched. See // wiki/concepts/shift.md. const money = (m: number, cur: string | null) => formatMoney(m, cur ?? ""); function StatusBadge({ status }: { status: MovementStatus }) { const { t } = useTranslation(); const cls = status === "authorized" ? "border-term-green/60 text-term-green" : status === "denied" ? "border-term-red/60 text-term-red" : "border-term-amber/60 text-term-amber"; return ( {t(`drawer.status.${status}`)} ); } export function DrawerManager({ canCreate, canReview }: { canCreate: boolean; canReview: boolean }) { const { t } = useTranslation(); const qc = useQueryClient(); // Reviewers can filter the list (the pending queue); operators always see their own, all. const [statusFilter, setStatusFilter] = useState(""); const q = useQuery({ queryKey: ["drawer", "movements", canReview ? statusFilter : ""], queryFn: () => fetchDrawerMovements(canReview && statusFilter ? statusFilter : undefined), }); const movements = q.data?.movements ?? []; const pendingCount = movements.filter((m) => m.status === "pending").length; return (
{canCreate && void qc.invalidateQueries({ queryKey: ["drawer"] })} />} 0 ? ( {t("drawer.pendingCount", { count: pendingCount })} ) : null } className="min-h-0 flex-1" >
{canReview && (
{(["", "pending", "authorized", "denied"] as const).map((s) => ( ))}
)}
{q.isLoading ? (
{t("common.loading")}
) : movements.length === 0 ? (
{t("drawer.empty")}
) : ( {canReview && } {canReview && {movements.map((m) => ( void qc.invalidateQueries({ queryKey: ["drawer"] })} /> ))}
{t("drawer.colWhen")} {t("drawer.colType")} {t("drawer.colAmount")} {t("drawer.colReason")}{t("drawer.colOperator")}{t("drawer.colStatus")}}
)}
); } function RecordPanel({ onDone }: { onDone: () => void }) { const { t } = useTranslation(); const [amount, setAmount] = useState(""); const [reason, setReason] = useState(""); const [msg, setMsg] = useState<{ text: string; ok: boolean } | null>(null); const record = useMutation({ mutationFn: (type: "cash_in" | "cash_out") => recordDrawerMovement({ type, amountMinor: Math.round(Number(amount) * 100), reason: reason.trim() }), onSuccess: (r) => { setMsg({ ok: true, text: t("drawer.recorded", { no: r.voucherNo, amount: money(r.balanceMinor, null) }) }); setAmount(""); setReason(""); onDone(); }, onError: (e) => setMsg({ ok: false, text: (e as Error).message }), }); function submit(type: "cash_in" | "cash_out") { setMsg(null); const major = Number(amount); if (!Number.isFinite(major) || major <= 0) { setMsg({ ok: false, text: t("drawer.enterPositive") }); return; } record.mutate(type); } return (
setAmount(e.target.value)} placeholder={t("drawer.amount")} inputMode="decimal" /> setReason(e.target.value)} placeholder={t("drawer.reasonPlaceholder")} />
{t("drawer.recordHint")}
{msg && (
{msg.text}
)}
); } function MovementRow({ m, canReview, onReviewed }: { m: DrawerMovement; canReview: boolean; onReviewed: () => void }) { const { t } = useTranslation(); const [note, setNote] = useState(""); const [noteOpen, setNoteOpen] = useState(false); const review = useMutation({ mutationFn: (decision: "authorize" | "deny") => reviewDrawerMovement({ refId: m.id, decision, note: note.trim() || undefined }), onSuccess: onReviewed, }); // Direction sign for display: cash_in is +, cash_out is −. const signed = m.type === "cash_in" ? m.amountMinor : -m.amountMinor; return ( {formatRelativeDateTime(m.at, t)} {m.type === "cash_in" ? t("drawer.mandatArketimi") : t("drawer.mandatPagese")} {m.voucherNo && {m.voucherNo}} {money(signed, m.currency)} {m.reason || "—"} {canReview && {m.operator}} {m.status !== "pending" && m.reviewedBy && (
{m.reviewedBy} {m.reviewNote ? ` · ${m.reviewNote}` : ""}
)} {canReview && ( {m.status === "pending" ? (
{noteOpen && ( setNote(e.target.value)} placeholder={t("drawer.denyNotePlaceholder")} /> )} {review.isError && {(review.error as Error).message}}
) : null} )} ); }