- Drag-and-drop widget arrangement (main two-column zone + sidebar) with per-widget span resize, persisted to Users.preferences.dashboard.layout via saveDashboardLayout (migration included) - New personal widgets: Wallet, Open Negotiations, My Tickets (View all), Recent Notifications (per-row mark as read) - Widgets that are empty or not visible to the viewer are omitted so they never render as empty drag targets - Registers both pending migrations and regenerates payload types/schema
95 lines
3.8 KiB
TypeScript
95 lines
3.8 KiB
TypeScript
import Link from "next/link";
|
||
import type { MarketListing, MarketNegotiation } from "@/payload-types";
|
||
import { Item, ItemContent, ItemHeader, ItemTitle } from "@/components/ui/item";
|
||
import { ChevronRightIcon, HandshakeIcon } from "lucide-react";
|
||
import { formatAmount, type CurrencyConfig } from "@/lib/banking/format";
|
||
|
||
interface OpenNegotiationsProps {
|
||
readonly negotiations: MarketNegotiation[];
|
||
readonly currentUserId: number;
|
||
readonly currencyConfig: CurrencyConfig;
|
||
}
|
||
|
||
function listingOf(negotiation: MarketNegotiation): MarketListing | null {
|
||
return typeof negotiation.listing === "object" && negotiation.listing
|
||
? (negotiation.listing as MarketListing)
|
||
: null;
|
||
}
|
||
|
||
function assetNameOf(listing: MarketListing | null): string {
|
||
const asset = listing?.asset;
|
||
return typeof asset === "object" && asset ? asset.name : "Unknown item";
|
||
}
|
||
|
||
function buyerIdOf(negotiation: MarketNegotiation): number | null {
|
||
return typeof negotiation.buyer === "object" && negotiation.buyer
|
||
? negotiation.buyer.id
|
||
: negotiation.buyer;
|
||
}
|
||
|
||
export function OpenNegotiations({
|
||
negotiations,
|
||
currentUserId,
|
||
currencyConfig,
|
||
}: OpenNegotiationsProps) {
|
||
// Deliberately hidden when there is nothing on the table (same convention as
|
||
// NegotiationPanel on the market page).
|
||
if (negotiations.length === 0) return null;
|
||
|
||
const fmt = (n: number) => formatAmount(n, currencyConfig);
|
||
|
||
return (
|
||
<Item variant="outline">
|
||
<ItemHeader>
|
||
<div className="flex items-center gap-2">
|
||
<HandshakeIcon className="size-4 text-muted-foreground" />
|
||
<ItemTitle>Negotiations</ItemTitle>
|
||
<span className="text-xs text-muted-foreground">{negotiations.length} open</span>
|
||
</div>
|
||
<Link
|
||
href="/logistics/market"
|
||
className="text-xs text-muted-foreground transition-colors hover:text-primary"
|
||
>
|
||
Market
|
||
</Link>
|
||
</ItemHeader>
|
||
<ItemContent>
|
||
<div className="flex flex-col gap-1">
|
||
{negotiations.map((negotiation) => {
|
||
const listing = listingOf(negotiation);
|
||
const role = buyerIdOf(negotiation) === currentUserId ? "buyer" : "seller";
|
||
const theirProposal =
|
||
(role === "buyer" && negotiation.proposedBy === "seller") ||
|
||
(role === "seller" && negotiation.proposedBy === "buyer");
|
||
const quantity = negotiation.quantity ?? listing?.quantity ?? 1;
|
||
|
||
return (
|
||
<Link
|
||
key={negotiation.id}
|
||
href="/logistics/market"
|
||
className="group flex items-center gap-3 rounded-md p-2 transition-colors hover:bg-muted/50"
|
||
>
|
||
<HandshakeIcon className="size-4 shrink-0 text-muted-foreground" />
|
||
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||
<div className="flex items-center gap-2">
|
||
<span className="truncate text-sm font-medium transition-colors group-hover:text-primary">
|
||
{assetNameOf(listing)}
|
||
</span>
|
||
<span className="shrink-0 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
||
{role === "buyer" ? "Buying" : "Selling"}
|
||
</span>
|
||
</div>
|
||
<span className="truncate text-xs text-muted-foreground">
|
||
{fmt(negotiation.amount ?? 0)} each · {quantity}× ·{" "}
|
||
{theirProposal ? "their offer" : "your offer"}
|
||
</span>
|
||
</div>
|
||
<ChevronRightIcon className="size-3 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100" />
|
||
</Link>
|
||
);
|
||
})}
|
||
</div>
|
||
</ItemContent>
|
||
</Item>
|
||
);
|
||
}
|