Files
Moonbase/src/components/inventory/attachment-upload.tsx
tonym 63569aabd3 v0.15.0 — Inventory depth (HomeBox parity) + Locations page
Rounds out Inventory toward HomeBox, and gives Locations a real home.

Inventory:
- Attach manuals, receipts, and warranty docs (any file), not just photos —
  attachment uploader now has a kind picker; WARRANTY kind added
- Richer purchase/warranty: purchasedFrom, warrantyDetails, lifetimeWarranty,
  insured (item dialog + detail facts + header badge)
- Schedule recurring maintenance from an item (creates a Task linked via
  itemId); item detail shows its maintenance with overdue flagged. Task create
  now accepts itemId/animalId

Locations:
- New Locations page + nav: browse/add/rename/move/delete the whole place tree
  (LocationManager over the existing /api/locations routes); shows plant/item
  counts per place. The spot to merge near-dupes before dropping legacy zone

Verified end-to-end against a running server (new fields round-trip, maintenance
task links to item, MANUAL attachment stored). tsc + 33 tests + build green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 00:34:07 -05:00

64 lines
2.4 KiB
TypeScript

"use client";
import { useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useToast } from "@/hooks/use-toast";
import { Paperclip, ChevronDown } from "lucide-react";
const KINDS: { value: string; label: string; accept: string }[] = [
{ value: "PHOTO", label: "Photo", accept: "image/*" },
{ value: "MANUAL", label: "Manual", accept: ".pdf,image/*" },
{ value: "RECEIPT", label: "Receipt", accept: ".pdf,image/*" },
{ value: "WARRANTY", label: "Warranty doc", accept: ".pdf,image/*" },
];
export function AttachmentUpload({ itemId }: { itemId: string }) {
const [busy, setBusy] = useState(false);
const [kind, setKind] = useState("PHOTO");
const inputRef = useRef<HTMLInputElement>(null);
const router = useRouter();
const { toast } = useToast();
function pick(k: string) {
setKind(k);
// Let state settle, then open the picker with the right accept filter.
requestAnimationFrame(() => inputRef.current?.click());
}
async function onFile(file: File) {
setBusy(true);
const form = new FormData();
form.append("file", file);
form.append("kind", kind);
const res = await fetch(`/api/v1/items/${itemId}/attachments`, { method: "POST", body: form });
setBusy(false);
if (res.ok) { toast({ title: "Attached" }); router.refresh(); }
else toast({ title: "Error", description: "Upload failed.", variant: "destructive" });
}
const accept = KINDS.find((k) => k.value === kind)?.accept ?? "*";
return (
<>
<input ref={inputRef} type="file" accept={accept} className="hidden"
onChange={(e) => { const f = e.target.files?.[0]; if (f) onFile(f); e.target.value = ""; }} />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="outline" disabled={busy}>
<Paperclip className="h-4 w-4 mr-1" /> {busy ? "Uploading…" : "Attach"} <ChevronDown className="h-3 w-3 ml-1" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{KINDS.map((k) => (
<DropdownMenuItem key={k.value} onClick={() => pick(k.value)}>{k.label}</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</>
);
}