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>
64 lines
2.4 KiB
TypeScript
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>
|
|
</>
|
|
);
|
|
}
|