Files
2026-04-05 00:43:23 +05:30

294 lines
13 KiB
TypeScript
Raw Permalink 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.
"use client";
import { useEffect, useState, useCallback, useMemo } from "react";
import Link from "next/link";
import { api } from "@/lib/api";
import type { Negotiation } from "@/lib/types";
import { FEATURE_LABELS, STATUS_COLORS, STATUS_LABELS, relativeTime } from "@/lib/utils";
import Sidebar from "@/components/Sidebar";
function Icon({ name, className = "" }: { name: string; className?: string }) {
return <span className={`material-symbols-outlined ${className}`}>{name}</span>;
}
const FEATURE_ICONS: Record<string, string> = {
scheduling: "calendar_month",
expenses: "account_balance_wallet",
freelance: "work",
roommate: "home",
trip: "flight_takeoff",
marketplace: "store",
collaborative: "groups",
conflict: "gavel",
generic: "hub",
};
const STATUS_FILTERS = [
{ key: "all", label: "All Statuses", dot: "" },
{ key: "resolved", label: "Resolved", dot: "bg-emerald-400" },
{ key: "escalated", label: "Escalated", dot: "bg-rose-400" },
{ key: "active", label: "In Progress", dot: "bg-amber-400" },
{ key: "pending", label: "Pending", dot: "bg-slate-400" },
];
const STATUS_BADGE: Record<string, string> = {
resolved: "text-emerald-400 bg-emerald-500/10 border-emerald-500/20",
escalated: "text-rose-400 bg-rose-500/10 border-rose-500/20",
active: "text-amber-400 bg-amber-500/10 border-amber-500/20",
pending: "text-slate-400 bg-white/5 border-white/10",
};
const PAGE_SIZE = 8;
export default function HistoryPage() {
const [negotiations, setNegotiations] = useState<Negotiation[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [page, setPage] = useState(1);
const load = useCallback(async () => {
try {
const res = await api.negotiations();
setNegotiations(res.negotiations);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
const filtered = useMemo(() => {
return negotiations.filter((n) => {
const matchStatus = statusFilter === "all" || n.status === statusFilter;
const matchSearch =
!search ||
n.id.toLowerCase().includes(search.toLowerCase()) ||
(FEATURE_LABELS[n.feature_type] ?? n.feature_type)
.toLowerCase()
.includes(search.toLowerCase());
return matchStatus && matchSearch;
});
}, [negotiations, statusFilter, search]);
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
const paginated = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
const getFairness = (n: Negotiation) => {
const r = (n as any).result;
if (r?.fairness_score != null) return Math.round(r.fairness_score * 100);
return null;
};
return (
<div className="flex h-screen w-full overflow-hidden bg-[#020105] text-slate-300 relative">
<div className="absolute inset-0 bg-grid-subtle opacity-20 pointer-events-none" />
<Sidebar />
<main className="flex-1 flex flex-col h-full overflow-hidden relative z-10">
{/* Top bar */}
<header className="h-16 flex items-center justify-between px-6 bg-[#050505]/80 backdrop-blur-md border-b border-white/5 sticky top-0 z-30 shrink-0">
<div>
<h2 className="text-base font-medium text-white tracking-tight">
Negotiation History
</h2>
<p className="text-[10px] text-slate-600 mt-0.5">
{filtered.length} record{filtered.length !== 1 ? "s" : ""} found
</p>
</div>
<Link
href="/"
className="flex items-center gap-2 px-4 py-2 rounded-lg bg-[#B7A6FB] text-[#020105] text-xs font-bold hover:brightness-110 transition-all"
>
<Icon name="add" className="text-sm" />
New Request
</Link>
</header>
<div className="flex-1 overflow-y-auto p-6">
{/* Search + Filters */}
<div className="glass-card rounded-xl p-4 mb-6">
<div className="relative mb-4">
<Icon
name="search"
className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500 text-lg"
/>
<input
type="text"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
placeholder="Search by ID or feature type..."
className="w-full bg-white/5 border border-white/10 rounded-lg pl-10 pr-4 py-2.5 text-sm text-white placeholder-slate-600 focus:outline-none focus:border-[#B7A6FB]/40 transition-all"
/>
</div>
<div className="flex flex-wrap gap-2">
{STATUS_FILTERS.map((f) => (
<button
key={f.key}
onClick={() => { setStatusFilter(f.key); setPage(1); }}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium transition-all border ${
statusFilter === f.key
? "bg-[#B7A6FB] text-[#020105] border-[#B7A6FB]"
: "bg-white/5 text-slate-400 border-white/10 hover:border-[#B7A6FB]/30 hover:text-white"
}`}
>
{f.dot && (
<span className={`size-1.5 rounded-full ${f.dot}`} />
)}
{f.label}
</button>
))}
</div>
</div>
{/* Table */}
{loading ? (
<div className="flex items-center justify-center h-48 text-slate-600">
<Icon name="progress_activity" className="animate-spin text-3xl text-[#B7A6FB]" />
</div>
) : paginated.length === 0 ? (
<div className="flex flex-col items-center justify-center h-48 gap-3 text-slate-600">
<Icon name="search_off" className="text-4xl" />
<p className="text-sm">No negotiations match your filters.</p>
</div>
) : (
<div className="glass-card rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/5 text-[10px] text-slate-500 uppercase tracking-wider font-mono">
<th className="text-left px-5 py-3">Feature</th>
<th className="text-left px-5 py-3">Participants</th>
<th className="text-left px-5 py-3">Status</th>
<th className="text-left px-5 py-3">Fairness</th>
<th className="text-left px-5 py-3">Date</th>
<th className="text-right px-5 py-3">Actions</th>
</tr>
</thead>
<tbody>
{paginated.map((neg, i) => {
const fairness = getFairness(neg);
return (
<tr
key={neg.id}
className={`border-b border-white/5 hover:bg-[#B7A6FB]/5 transition-colors group ${
i === paginated.length - 1 ? "border-b-0" : ""
}`}
>
{/* Feature */}
<td className="px-5 py-3.5">
<div className="flex items-center gap-3">
<div className="size-8 rounded-lg bg-white/5 border border-white/10 flex items-center justify-center shrink-0">
<Icon
name={FEATURE_ICONS[neg.feature_type] ?? "hub"}
className="text-[#B7A6FB] text-base"
/>
</div>
<div>
<p className="text-white font-medium text-xs">
{FEATURE_LABELS[neg.feature_type] ?? neg.feature_type}
</p>
<p className="text-[10px] text-slate-600 font-mono">
#{neg.id.slice(0, 8)}
</p>
</div>
</div>
</td>
{/* Participants */}
<td className="px-5 py-3.5">
<div className="flex -space-x-2">
{[...Array(Math.min(neg.participant_count ?? 0, 3))].map((_, j) => (
<div
key={j}
className="w-6 h-6 rounded-full bg-slate-800 border border-black flex items-center justify-center text-[8px] text-white ring-1 ring-white/10"
>
{String.fromCharCode(65 + j)}
</div>
))}
</div>
</td>
{/* Status */}
<td className="px-5 py-3.5">
<span
className={`text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded border ${
STATUS_BADGE[neg.status] ?? "text-slate-400 bg-white/5 border-white/10"
}`}
>
{STATUS_LABELS[neg.status] ?? neg.status}
</span>
</td>
{/* Fairness */}
<td className="px-5 py-3.5">
{fairness != null ? (
<div className="flex items-center gap-2">
<div className="w-20 h-1 bg-white/10 rounded-full overflow-hidden">
<div
className="h-full bg-[#B7A6FB] rounded-full"
style={{ width: `${fairness}%` }}
/>
</div>
<span className="text-[10px] text-slate-400 font-mono">
{fairness}%
</span>
</div>
) : (
<span className="text-[10px] text-slate-600 font-mono"></span>
)}
</td>
{/* Date */}
<td className="px-5 py-3.5">
<span className="text-[10px] text-slate-500 font-mono">
{relativeTime(neg.created_at)}
</span>
</td>
{/* Actions */}
<td className="px-5 py-3.5 text-right">
<Link
href={`/negotiation/${neg.id}`}
className="inline-flex items-center justify-center size-8 rounded-lg bg-white/5 border border-white/10 text-slate-500 hover:text-[#B7A6FB] hover:border-[#B7A6FB]/30 transition-all"
title="View details"
>
<Icon name="visibility" className="text-base" />
</Link>
</td>
</tr>
);
})}
</tbody>
</table>
{/* Pagination */}
<div className="flex items-center justify-between px-5 py-3 border-t border-white/5 text-[10px] text-slate-500 font-mono">
<span>
Showing {Math.min((page - 1) * PAGE_SIZE + 1, filtered.length)}
{Math.min(page * PAGE_SIZE, filtered.length)} of {filtered.length} negotiations
</span>
<div className="flex items-center gap-1">
<button
onClick={() => setPage((p) => Math.max(1, p - 1))}
disabled={page === 1}
className="size-7 rounded flex items-center justify-center bg-white/5 border border-white/10 hover:border-[#B7A6FB]/30 hover:text-[#B7A6FB] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
<Icon name="chevron_left" className="text-sm" />
</button>
<button
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
className="size-7 rounded flex items-center justify-center bg-white/5 border border-white/10 hover:border-[#B7A6FB]/30 hover:text-[#B7A6FB] disabled:opacity-30 disabled:cursor-not-allowed transition-all"
>
<Icon name="chevron_right" className="text-sm" />
</button>
</div>
</div>
</div>
)}
</div>
</main>
</div>
);
}