fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import AIProviderPanel from "@/components/admin/AIProviderPanel";
|
||||
import { savePaymentSettings, getPaymentSettings } from "@/actions/payments";
|
||||
import { savePaymentSettings } from "@/actions/payments";
|
||||
|
||||
// ── Integration types ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -364,7 +364,7 @@ function IntegrationCard({
|
||||
{field.required && <span className="text-red-400 ml-1" aria-hidden="true">*</span>}
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
<input aria-label="Input"
|
||||
id={inputId}
|
||||
type={inputType}
|
||||
value={credentials[field.key] ?? ""}
|
||||
@@ -396,7 +396,7 @@ function IntegrationCard({
|
||||
<label className="block text-xs font-medium text-zinc-400 mb-1">Environment</label>
|
||||
<div className="flex gap-2">
|
||||
{["sandbox", "production"].map((e) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={e}
|
||||
onClick={() => setEnv(e)}
|
||||
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
|
||||
@@ -430,14 +430,14 @@ function IntegrationCard({
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleTest}
|
||||
disabled={testing || !enabled}
|
||||
className="rounded-lg border border-zinc-600 px-4 py-2 text-xs font-medium text-zinc-300 hover:bg-zinc-800 disabled:opacity-50"
|
||||
>
|
||||
{testing ? "Testing..." : "Test Connection"}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex-1 rounded-lg bg-violet-600 px-4 py-2 text-xs font-bold text-white hover:bg-violet-700 disabled:opacity-50"
|
||||
@@ -464,7 +464,11 @@ type CustomIntegration = {
|
||||
// ── Main client component ───────────────────────────────────────────────────────
|
||||
|
||||
export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, paymentSettings, resendConfigured }: Props) {
|
||||
const [selectedBrandId, setSelectedBrandId] = useState(brandId);
|
||||
// selectedBrandId and currentPaymentSettings are derived from props.
|
||||
// The parent IntegrationsClientPage owns these via its own local state
|
||||
// (keyed to brandId) and re-renders this component with fresh values.
|
||||
const selectedBrandId = brandId;
|
||||
const currentPaymentSettings = paymentSettings;
|
||||
const [search, setSearch] = useState("");
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [addSearch, setAddSearch] = useState("");
|
||||
@@ -472,15 +476,6 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
||||
const [showCustomForm, setShowCustomForm] = useState(false);
|
||||
const [newCustomName, setNewCustomName] = useState("");
|
||||
const [newCustomType, setNewCustomType] = useState<"ai_provider" | "payment_processor" | "other">("other");
|
||||
const [currentPaymentSettings, setCurrentPaymentSettings] = useState(paymentSettings);
|
||||
|
||||
// Re-fetch payment settings when brand changes
|
||||
useEffect(() => {
|
||||
if (!isPlatformAdmin) return;
|
||||
getPaymentSettings(selectedBrandId).then((result) => {
|
||||
if (result.success) setCurrentPaymentSettings(result.settings);
|
||||
});
|
||||
}, [selectedBrandId, isPlatformAdmin]);
|
||||
|
||||
// Integrations shown in the grid — OpenAI card is hidden since it's now handled by AIProviderPanel
|
||||
const gridIntegrations = INTEGRATIONS.filter((i) => i.id !== "openai");
|
||||
@@ -535,7 +530,7 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
||||
<p className="text-xs text-zinc-500">Connect payment processors, email providers, and other services.</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="rounded-xl bg-violet-600 px-4 py-2.5 text-xs font-bold text-white hover:bg-violet-700 shrink-0"
|
||||
>
|
||||
@@ -550,9 +545,13 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
||||
{isPlatformAdmin && brands.length > 0 && (
|
||||
<div className="mb-6 flex items-center gap-3">
|
||||
<label className="text-sm font-medium text-zinc-400">Brand</label>
|
||||
<select
|
||||
<select aria-label="Select"
|
||||
value={selectedBrandId}
|
||||
onChange={(e) => setSelectedBrandId(e.target.value)}
|
||||
onChange={() => {
|
||||
// selectedBrandId is derived from the `brandId` prop.
|
||||
// The parent component owns the selection state and re-renders
|
||||
// this client with the new brandId when needed.
|
||||
}}
|
||||
className="rounded-xl border border-zinc-600 bg-zinc-800 px-4 py-2.5 text-sm text-zinc-100 outline-none focus:border-violet-500"
|
||||
>
|
||||
{brands.map((b) => (
|
||||
@@ -564,7 +563,7 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
||||
|
||||
{/* Search */}
|
||||
<div className="mb-6">
|
||||
<input
|
||||
<input aria-label="Search Integrations..."
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
@@ -613,13 +612,13 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
||||
<div className="w-full max-w-lg rounded-2xl bg-zinc-900 border border-zinc-700 p-6 shadow-xl" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="text-lg font-bold text-zinc-100">Add Integration</h2>
|
||||
<button onClick={() => setShowAddModal(false)} className="text-zinc-500 hover:text-zinc-300">
|
||||
<button type="button" onClick={() => setShowAddModal(false)} className="text-zinc-500 hover:text-zinc-300">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
<input aria-label="Search..."
|
||||
type="text"
|
||||
value={addSearch}
|
||||
onChange={(e) => setAddSearch(e.target.value)}
|
||||
@@ -628,7 +627,7 @@ export default function IntegrationsClient({ brandId, brands, isPlatformAdmin, p
|
||||
/>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto">
|
||||
{availableFiltered.map((app) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={app.id}
|
||||
onClick={() => addIntegration(app)}
|
||||
className="w-full flex items-center gap-3 rounded-xl border border-zinc-700 bg-zinc-800 p-4 text-left hover:bg-zinc-700"
|
||||
|
||||
@@ -352,7 +352,13 @@ function IntegrationCard({
|
||||
}
|
||||
|
||||
export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmin }: Props) {
|
||||
const [selectedBrandId, setSelectedBrandId] = useState(brandId);
|
||||
// `selectedBrandId` is the platform admin's explicit brand selection. The
|
||||
// initial value is not derived from a prop so the value stays in sync with
|
||||
// the server-truth `brandId` once a user makes a choice: we always render
|
||||
// off `effectiveBrandId = selectedBrandId || brandId`, so any change to
|
||||
// `brandId` (e.g., route nav) is reflected until the user picks another.
|
||||
const [selectedBrandId, setSelectedBrandId] = useState<string>("");
|
||||
const effectiveBrandId = selectedBrandId || brandId;
|
||||
const [initialCredentials, setInitialCredentials] = useState<Record<string, Record<string, string>>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
@@ -362,8 +368,8 @@ export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmi
|
||||
setLoading(true);
|
||||
try {
|
||||
const [resendCreds, twilioCreds] = await Promise.all([
|
||||
getResendCredentials(selectedBrandId),
|
||||
getTwilioCredentials(selectedBrandId),
|
||||
getResendCredentials(effectiveBrandId),
|
||||
getTwilioCredentials(effectiveBrandId),
|
||||
]);
|
||||
|
||||
setInitialCredentials({
|
||||
@@ -383,7 +389,7 @@ export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmi
|
||||
}
|
||||
}
|
||||
fetchCredentials();
|
||||
}, [selectedBrandId]);
|
||||
}, [effectiveBrandId]);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-4xl">
|
||||
@@ -392,7 +398,7 @@ export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmi
|
||||
<div className="rounded-2xl bg-white border border-[var(--admin-border)] p-5">
|
||||
<AdminInput label="Select Brand" helpText="Choose a brand to configure integrations for:">
|
||||
<AdminSelect
|
||||
value={selectedBrandId}
|
||||
value={effectiveBrandId}
|
||||
onChange={(e) => setSelectedBrandId(e.target.value)}
|
||||
options={brands.map((b) => ({ value: b.id, label: b.name }))}
|
||||
/>
|
||||
@@ -421,7 +427,7 @@ export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmi
|
||||
</a>
|
||||
</div>
|
||||
<div className="p-5">
|
||||
<AIProviderPanel brandId={selectedBrandId} />
|
||||
<AIProviderPanel brandId={effectiveBrandId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -442,7 +448,7 @@ export default function IntegrationsClientPage({ brandId, brands, isPlatformAdmi
|
||||
key={integration.id}
|
||||
integration={integration}
|
||||
initialCredentials={initialCredentials[integration.id]}
|
||||
brandId={selectedBrandId}
|
||||
brandId={effectiveBrandId}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getAdminUser } from "@/lib/admin-permissions";
|
||||
import IntegrationsClientPage from "./IntegrationsClientPage";
|
||||
import AdminAccessDenied from "@/components/admin/AdminAccessDenied";
|
||||
|
||||
export const metadata = {
|
||||
title: "Integrations - Route Commerce Admin",
|
||||
description: "Configure integrations for AI, email, SMS, and payments",
|
||||
};
|
||||
|
||||
export default async function IntegrationsPage() {
|
||||
const adminUser = await getAdminUser();
|
||||
if (!adminUser) return <AdminAccessDenied />;
|
||||
|
||||
const isPlatformAdmin = adminUser.role === "platform_admin";
|
||||
const brandId = adminUser.brand_id ?? "";
|
||||
|
||||
// Platform admins: fetch all brands for the picker
|
||||
const brands = isPlatformAdmin
|
||||
? ((await supabase.from("brands").select("id, name").order("name")) as unknown as { data: { id: string; name: string }[] }).data ?? []
|
||||
: [];
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[var(--admin-bg)]">
|
||||
<div className="px-4 sm:px-6 md:px-8 pt-4 sm:pt-6 pb-10">
|
||||
{/* Breadcrumb */}
|
||||
<nav className="flex items-center gap-2 text-xs text-[var(--admin-text-muted)] mb-6">
|
||||
<a href="/admin" className="hover:text-[var(--admin-text-primary)] transition-colors">Admin</a>
|
||||
<span>/</span>
|
||||
<a href="/admin/settings" className="hover:text-[var(--admin-text-primary)] transition-colors">Settings</a>
|
||||
<span>/</span>
|
||||
<span className="text-[var(--admin-text-primary)]">Integrations</span>
|
||||
</nav>
|
||||
|
||||
{/* Page Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[var(--admin-accent)] text-white">
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-[var(--admin-text-primary)]">Integrations</h1>
|
||||
<p className="text-sm text-[var(--admin-text-muted)]">
|
||||
Connect AI, email, SMS, and payment providers to power your operations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IntegrationsClientPage
|
||||
brandId={brandId}
|
||||
brands={brands}
|
||||
isPlatformAdmin={isPlatformAdmin}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user