1
0
Fork 0
polaris-task-force/src/components/frontend/dashboard/OpenNegotiations.tsx
Z8MB1E f0af778713 feat(dashboard): customizable dashboard with personal widgets
- 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
2026-09-10 20:03:27 -04:00

95 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}