feat(brand): replace placeholder mark with logo + favicon, fix connect button
This commit is contained in:
@@ -5,6 +5,14 @@ import { useTranslations, useFormatter } from "next-intl";
|
||||
import type { PiecedTenant, TenantRequest } from "@/types";
|
||||
import { StatusBadge } from "@/components/ui/status-badge";
|
||||
import { Modal } from "@/components/ui/modal";
|
||||
import {
|
||||
applyTableView,
|
||||
nextSort,
|
||||
SearchInput,
|
||||
SortableTh,
|
||||
Pagination,
|
||||
type SortState,
|
||||
} from "@/components/admin/table-controls";
|
||||
import { formatDateTime, formatRelative } from "@/lib/format";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -53,6 +61,21 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
|
||||
// Shared
|
||||
const [error, setError] = useState("");
|
||||
|
||||
// Client-side table view state (search / sort / page) for each tab.
|
||||
const [reqSearch, setReqSearch] = useState("");
|
||||
const [reqSort, setReqSort] = useState<SortState>({
|
||||
key: "created",
|
||||
dir: "desc",
|
||||
});
|
||||
const [reqPage, setReqPage] = useState(1);
|
||||
|
||||
const [tenSearch, setTenSearch] = useState("");
|
||||
const [tenSort, setTenSort] = useState<SortState>({
|
||||
key: "created",
|
||||
dir: "desc",
|
||||
});
|
||||
const [tenPage, setTenPage] = useState(1);
|
||||
// Action-scoped error — shown inside the active confirmation modal so
|
||||
// a failed approve/reject/delete surfaces next to the action that
|
||||
// caused it (and keeps the modal open), rather than as a detached
|
||||
@@ -246,6 +269,53 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
|
||||
const pendingCount = requests.filter((r) => r.status === "pending").length;
|
||||
|
||||
// Derived table views: search → sort → paginate, applied client-side
|
||||
// on top of the already-fetched lists.
|
||||
const reqView = applyTableView(requests, {
|
||||
search: reqSearch,
|
||||
searchOf: (r) => [
|
||||
r.companyName,
|
||||
r.contactName,
|
||||
r.contactEmail,
|
||||
r.agentName,
|
||||
r.tenantName,
|
||||
],
|
||||
sort: reqSort,
|
||||
sortOf: (r, key) =>
|
||||
key === "company"
|
||||
? r.companyName || ""
|
||||
: key === "status"
|
||||
? r.status || ""
|
||||
: r.createdAt || "",
|
||||
page: reqPage,
|
||||
});
|
||||
|
||||
const tenView = applyTableView(tenants, {
|
||||
search: tenSearch,
|
||||
searchOf: (tn) => [
|
||||
tn.metadata.name,
|
||||
tn.spec.displayName,
|
||||
tn.spec.agentName,
|
||||
],
|
||||
sort: tenSort,
|
||||
sortOf: (tn, key) =>
|
||||
key === "name"
|
||||
? tn.spec.displayName || tn.metadata.name
|
||||
: key === "phase"
|
||||
? tn.status?.phase || "Pending"
|
||||
: tn.metadata.creationTimestamp || "",
|
||||
page: tenPage,
|
||||
});
|
||||
|
||||
const onReqSort = (key: string) => {
|
||||
setReqSort((s) => nextSort(s, key));
|
||||
setReqPage(1);
|
||||
};
|
||||
const onTenSort = (key: string) => {
|
||||
setTenSort((s) => nextSort(s, key));
|
||||
setTenPage(1);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Tab bar */}
|
||||
@@ -315,20 +385,33 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
{/* ───── REQUESTS TAB ───── */}
|
||||
{tab === "requests" && (
|
||||
<>
|
||||
<div className="flex gap-1.5 mb-4 flex-wrap">
|
||||
{FILTERS.map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => setFilter(f)}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
filter === f
|
||||
? "bg-accent text-surface-0"
|
||||
: "bg-surface-2 text-text-muted hover:text-text-secondary border border-border"
|
||||
}`}
|
||||
>
|
||||
{t(`filter_${f}`)}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex items-center justify-between gap-3 mb-4 flex-wrap">
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{FILTERS.map((f) => (
|
||||
<button
|
||||
key={f}
|
||||
onClick={() => {
|
||||
setFilter(f);
|
||||
setReqPage(1);
|
||||
}}
|
||||
className={`px-3 py-1 text-xs rounded-full transition-colors ${
|
||||
filter === f
|
||||
? "bg-accent text-surface-0"
|
||||
: "bg-surface-2 text-text-muted hover:text-text-secondary border border-border"
|
||||
}`}
|
||||
>
|
||||
{t(`filter_${f}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<SearchInput
|
||||
value={reqSearch}
|
||||
onChange={(v) => {
|
||||
setReqSearch(v);
|
||||
setReqPage(1);
|
||||
}}
|
||||
placeholder={t("searchRequestsPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loadingRequests ? (
|
||||
@@ -340,15 +423,22 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
|
||||
<p className="text-text-secondary text-sm">{t("noRequests")}</p>
|
||||
</div>
|
||||
) : reqView.total === 0 ? (
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
|
||||
<p className="text-text-secondary text-sm">{t("noMatches")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-surface-1 border border-border rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("company")}
|
||||
</th>
|
||||
<SortableTh
|
||||
label={t("company")}
|
||||
sortKey="company"
|
||||
sort={reqSort}
|
||||
onSort={onReqSort}
|
||||
/>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("contact")}
|
||||
</th>
|
||||
@@ -358,19 +448,26 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden lg:table-cell">
|
||||
{t("packages")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("status")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
|
||||
{t("submitted")}
|
||||
</th>
|
||||
<SortableTh
|
||||
label={t("status")}
|
||||
sortKey="status"
|
||||
sort={reqSort}
|
||||
onSort={onReqSort}
|
||||
/>
|
||||
<SortableTh
|
||||
label={t("submitted")}
|
||||
sortKey="created"
|
||||
sort={reqSort}
|
||||
onSort={onReqSort}
|
||||
className="hidden md:table-cell"
|
||||
/>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("actions")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{requests.map((req) => (
|
||||
{reqView.paged.map((req) => (
|
||||
<tr
|
||||
key={req.id}
|
||||
className="border-b border-border last:border-0 hover:bg-surface-2/50 transition-colors"
|
||||
@@ -506,6 +603,12 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination
|
||||
page={reqView.page}
|
||||
totalPages={reqView.totalPages}
|
||||
total={reqView.total}
|
||||
onPage={setReqPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -543,6 +646,17 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mb-4">
|
||||
<SearchInput
|
||||
value={tenSearch}
|
||||
onChange={(v) => {
|
||||
setTenSearch(v);
|
||||
setTenPage(1);
|
||||
}}
|
||||
placeholder={t("searchTenantsPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loadingTenants ? (
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
|
||||
<div className="h-5 w-5 border-2 border-accent border-t-transparent rounded-full animate-spin mx-auto mb-2" />
|
||||
@@ -552,37 +666,51 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
|
||||
<p className="text-text-secondary text-sm">{t("noTenants")}</p>
|
||||
</div>
|
||||
) : tenView.total === 0 ? (
|
||||
<div className="bg-surface-1 border border-border rounded-xl p-12 text-center">
|
||||
<p className="text-text-secondary text-sm">{t("noMatches")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-surface-1 border border-border rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-border text-left">
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("name")}
|
||||
</th>
|
||||
<SortableTh
|
||||
label={t("name")}
|
||||
sortKey="name"
|
||||
sort={tenSort}
|
||||
onSort={onTenSort}
|
||||
/>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("displayName")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("phase")}
|
||||
</th>
|
||||
<SortableTh
|
||||
label={t("phase")}
|
||||
sortKey="phase"
|
||||
sort={tenSort}
|
||||
onSort={onTenSort}
|
||||
/>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
|
||||
{t("packages")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
|
||||
{t("spendChf")}
|
||||
</th>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted hidden md:table-cell">
|
||||
{t("created")}
|
||||
</th>
|
||||
<SortableTh
|
||||
label={t("created")}
|
||||
sortKey="created"
|
||||
sort={tenSort}
|
||||
onSort={onTenSort}
|
||||
className="hidden md:table-cell"
|
||||
/>
|
||||
<th className="px-4 py-3 text-xs font-semibold uppercase tracking-wider text-text-muted">
|
||||
{t("actions")}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tenants.map((tenant) => {
|
||||
{tenView.paged.map((tenant) => {
|
||||
const tenantSpend =
|
||||
health?.spend?.perTenant?.[tenant.metadata.name];
|
||||
return (
|
||||
@@ -680,6 +808,12 @@ export function AdminPanel({ initialTenants }: AdminPanelProps) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Pagination
|
||||
page={tenView.page}
|
||||
totalPages={tenView.totalPages}
|
||||
total={tenView.total}
|
||||
onPage={setTenPage}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user