feat(inventory): inline site selector above items table
Items page now fetches all active sites and passes them alongside
preferredSiteId to ItemsTable. A "Working Site" row appears at the
top of the table — selecting a site calls setPreferredSite, revalidates
the page, and shows distances in the vendor sub-rows. A status hint
("Distances shown from selected site") appears when a site is active;
"No site selected — distances hidden" is the empty-state label.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
79897c5b06
commit
4919b1d4e4
2 changed files with 54 additions and 12 deletions
|
|
@ -1,9 +1,10 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo, useTransition } from "react";
|
||||||
import { Search, X, ChevronDown, ChevronRight, MapPin, Tag } from "lucide-react";
|
import { Search, X, ChevronDown, ChevronRight, MapPin, Tag } from "lucide-react";
|
||||||
import { formatCurrency } from "@/lib/utils";
|
import { formatCurrency } from "@/lib/utils";
|
||||||
import { addToCart } from "@/lib/cart";
|
import { addToCart } from "@/lib/cart";
|
||||||
|
import { setPreferredSite } from "@/app/actions/site-preference";
|
||||||
|
|
||||||
type VendorOption = {
|
type VendorOption = {
|
||||||
vendorId: string;
|
vendorId: string;
|
||||||
|
|
@ -25,11 +26,24 @@ function formatDist(km: number) {
|
||||||
return km < 1 ? `${Math.round(km * 1000)} m` : `${km.toFixed(0)} km`;
|
return km < 1 ? `${Math.round(km * 1000)} m` : `${km.toFixed(0)} km`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ItemsTable({ items, hasSite }: { items: CatalogItem[]; hasSite: boolean }) {
|
type SiteOption = { id: string; name: string; code: string };
|
||||||
|
|
||||||
|
export function ItemsTable({
|
||||||
|
items,
|
||||||
|
hasSite,
|
||||||
|
sites = [],
|
||||||
|
preferredSiteId = null,
|
||||||
|
}: {
|
||||||
|
items: CatalogItem[];
|
||||||
|
hasSite: boolean;
|
||||||
|
sites?: SiteOption[];
|
||||||
|
preferredSiteId?: string | null;
|
||||||
|
}) {
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||||
const [sortBy, setSortBy] = useState<"distance" | "price">(hasSite ? "distance" : "price");
|
const [sortBy, setSortBy] = useState<"distance" | "price">(hasSite ? "distance" : "price");
|
||||||
const [added, setAdded] = useState<Record<string, boolean>>({});
|
const [added, setAdded] = useState<Record<string, boolean>>({});
|
||||||
|
const [sitePending, startSiteTransition] = useTransition();
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const q = query.toLowerCase().trim();
|
const q = query.toLowerCase().trim();
|
||||||
|
|
@ -79,6 +93,31 @@ export function ItemsTable({ items, hasSite }: { items: CatalogItem[]; hasSite:
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
{/* Site selector */}
|
||||||
|
{sites.length > 0 && (
|
||||||
|
<div className="flex items-center gap-3 rounded-lg border border-neutral-200 bg-white px-4 py-3">
|
||||||
|
<MapPin className="h-4 w-4 text-neutral-400 shrink-0" />
|
||||||
|
<label className="text-sm font-medium text-neutral-700 whitespace-nowrap">Working Site</label>
|
||||||
|
<select
|
||||||
|
value={preferredSiteId ?? ""}
|
||||||
|
disabled={sitePending}
|
||||||
|
onChange={(e) =>
|
||||||
|
startSiteTransition(() => setPreferredSite(e.target.value || null))
|
||||||
|
}
|
||||||
|
className="flex-1 max-w-xs rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-1.5 text-sm text-neutral-700 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20 disabled:opacity-60"
|
||||||
|
>
|
||||||
|
<option value="">No site selected — distances hidden</option>
|
||||||
|
{sites.map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>{s.name} ({s.code})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{sitePending && <span className="text-xs text-neutral-400">Updating…</span>}
|
||||||
|
{!sitePending && preferredSiteId && (
|
||||||
|
<span className="text-xs text-primary-600">Distances shown from selected site</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="relative flex-1 max-w-sm">
|
<div className="relative flex-1 max-w-sm">
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export default async function InventoryItemsPage() {
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
if (!session?.user) redirect("/login");
|
if (!session?.user) redirect("/login");
|
||||||
|
|
||||||
const [user, products] = await Promise.all([
|
const [user, products, sites] = await Promise.all([
|
||||||
db.user.findUnique({
|
db.user.findUnique({
|
||||||
where: { id: session.user.id },
|
where: { id: session.user.id },
|
||||||
include: {
|
include: {
|
||||||
|
|
@ -33,6 +33,11 @@ export default async function InventoryItemsPage() {
|
||||||
},
|
},
|
||||||
orderBy: { name: "asc" },
|
orderBy: { name: "asc" },
|
||||||
}),
|
}),
|
||||||
|
db.site.findMany({
|
||||||
|
where: { isActive: true },
|
||||||
|
orderBy: { name: "asc" },
|
||||||
|
select: { id: true, name: true, code: true },
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const site = user?.preferredSite ?? null;
|
const site = user?.preferredSite ?? null;
|
||||||
|
|
@ -61,16 +66,14 @@ export default async function InventoryItemsPage() {
|
||||||
<div className="max-w-6xl">
|
<div className="max-w-6xl">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h1 className="text-2xl font-semibold text-neutral-900">Browse Items</h1>
|
<h1 className="text-2xl font-semibold text-neutral-900">Browse Items</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-500">
|
<p className="mt-1 text-sm text-neutral-500">Search the catalogue and add items to your cart.</p>
|
||||||
Search the catalogue and add items to your cart.
|
|
||||||
{site ? (
|
|
||||||
<span className="ml-1 text-primary-600">Distances shown from {site.name}.</span>
|
|
||||||
) : (
|
|
||||||
<span className="ml-1 text-neutral-400">Select a site in the header to enable distance sorting.</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<ItemsTable items={items} hasSite={!!site} />
|
<ItemsTable
|
||||||
|
items={items}
|
||||||
|
hasSite={!!site}
|
||||||
|
sites={sites}
|
||||||
|
preferredSiteId={user?.preferredSiteId ?? null}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue