pelagia-portal/App/components/po/po-attachment-uploader.tsx
Hardik e481eb0a15
All checks were successful
PR checks / checks (pull_request) Successful in 50s
PR checks / integration (pull_request) Successful in 31s
feat(po): allow attachments in any state except rejected/cancelled
Broadens the feature-flagged attachment affordance (same flag,
NEXT_PUBLIC_CLOSED_PO_ATTACHMENTS_ENABLED) from CLOSED-only to **any PO state
except REJECTED / CANCELLED**, for the same roles: the PO's own submitter plus
Accounts / Manager / SuperUser.

- lib/permissions.ts: canAddClosedPoAttachment → canAddPoAttachment(role,
  status, { isSubmitter }); allows the submitter + ACCOUNTS/MANAGER/SUPERUSER
  in any non-voided state. REJECTED/CANCELLED are always refused.
- uploadPoDocuments: voided POs are refused regardless of the flag; with the
  flag on, uploads are restricted to those roles in any live state (the normal
  create/receipt actors qualify, so those flows keep working); with the flag
  off, the legacy behaviour stands (closed POs immutable).
- po-detail.tsx: the Attachments card now shows the uploader for any non-voided
  state when permitted (not just CLOSED).
- Renamed ClosedPoAttachmentUploader → PoAttachmentUploader and the test file
  to po-attachment-permissions.test.ts (flag-on matrix now covers live states +
  rejected/cancelled refusal). Docs updated (feature-flags, .env.example,
  CLAUDE.md).

Full unit + integration suites green; tsc clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 01:42:13 +05:30

55 lines
2 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { FileUploader } from "@/components/po/file-uploader";
import { uploadPoDocuments } from "@/app/actions/upload-po-documents";
/**
* Feature-flagged uploader shown on a PO's detail page so its submitter (or
* Accounts / Manager / SuperUser) can add documents after the fact — in any state
* except rejected/cancelled. Gating is decided server-side in po-detail.tsx; the
* server action re-checks the permission, so this component is only the UI.
*/
export function PoAttachmentUploader({ poId }: { poId: string }) {
const router = useRouter();
const [files, setFiles] = useState<File[]>([]);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
async function handleUpload() {
if (files.length === 0) return;
setBusy(true);
setError("");
const err = await uploadPoDocuments(poId, files);
if (err) {
setError(err.error);
setBusy(false);
return;
}
setFiles([]);
setBusy(false);
router.refresh();
}
return (
<div className="mt-5 border-t border-neutral-100 pt-4">
<p className="text-xs font-semibold uppercase tracking-wide text-neutral-500">Add attachments</p>
<p className="mt-0.5 text-xs text-neutral-400">
Attach any documents that are missing from this purchase order.
</p>
<div className="mt-3">
<FileUploader files={files} onChange={setFiles} disabled={busy} />
</div>
{error && <p className="mt-2 text-sm text-danger-700">{error}</p>}
<button
type="button"
onClick={handleUpload}
disabled={busy || files.length === 0}
className="mt-3 rounded-lg bg-primary-600 px-4 py-2 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-50"
>
{busy ? "Uploading…" : `Upload${files.length > 0 ? ` ${files.length} file${files.length > 1 ? "s" : ""}` : ""}`}
</button>
</div>
);
}