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:
Nora
2026-06-25 23:49:37 -06:00
parent 4d295ef062
commit 0ac4beaaa8
580 changed files with 52565 additions and 4953 deletions
+127 -107
View File
@@ -1,6 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { AdminCard } from "@/components/admin/design-system";
import { setAIProviderSettings } from "@/actions/integrations/ai-providers";
@@ -96,7 +96,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
</div>
<div className="mt-4 pt-4" style={{ borderTop: '1px solid var(--admin-border-light)' }}>
{tool.status === "available" && (
<button
<button type="button"
onClick={() => isConnected && onOpen(tool)}
disabled={!isConnected}
className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold transition-all"
@@ -113,7 +113,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
</button>
)}
{tool.status === "coming_soon" && (
<button disabled className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold cursor-not-allowed"
<button type="button" disabled className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold cursor-not-allowed"
style={{
background: 'rgba(0, 0, 0, 0.04)',
color: 'rgba(0, 0, 0, 0.4)',
@@ -122,7 +122,7 @@ function ToolCard({ tool, isConnected, onOpen }: ToolCardProps) {
</button>
)}
{tool.status === "experimental" && (
<button disabled className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold cursor-not-allowed"
<button type="button" disabled className="w-full rounded-xl px-4 py-2.5 text-sm font-semibold cursor-not-allowed"
style={{
background: 'rgba(171, 162, 120, 0.15)',
color: '#92781e',
@@ -194,7 +194,7 @@ function ConnectionStatusBanner({ isConnected, availableCount, onConfigure }: {
<p className="font-semibold" style={{ color: '#92781e' }}>AI Not Configured</p>
<p className="text-sm" style={{ color: '#a6953f' }}>Add your API key to enable AI tools.</p>
</div>
<button
<button type="button"
onClick={onConfigure}
className="rounded-xl px-4 py-2 text-sm font-bold transition-all"
style={{
@@ -320,28 +320,33 @@ const PROVIDERS: ProviderOption[] = [
// ── Model Input ───────────────────────────────────────────────────────────────
function ModelInput({
currentModel,
function ModelInput({
currentModel,
brandId,
onChange
}: {
onChange
}: {
currentModel: string;
brandId: string;
onChange: (model: string) => void;
}) {
const [customModel, setCustomModel] = useState(currentModel);
// Uncontrolled input: the editable buffer is the DOM input itself.
// The parent uses `key={currentModel}` so that when the prop changes
// externally, this component fully remounts and `defaultValue` is
// re-applied from the new prop — no stale copy.
const inputRef = useRef<HTMLInputElement>(null);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSave = async () => {
if (!customModel.trim()) return;
const value = inputRef.current?.value ?? "";
if (!value.trim()) return;
setSaving(true);
setError(null);
const result = await setAIProviderSettings(brandId, { model: customModel.trim() });
const result = await setAIProviderSettings(brandId, { model: value.trim() });
if (result.success) {
onChange(customModel.trim());
onChange(value.trim());
setSaved(true);
setTimeout(() => setSaved(false), 2000);
} else {
@@ -359,10 +364,11 @@ function ModelInput({
</h2>
</div>
<div className="flex gap-3">
<input
<input aria-label="., Gpt 4o, Claude 3 5 Sonnet 20241002"
ref={inputRef}
type="text"
value={customModel}
onChange={(e) => setCustomModel(e.target.value)}
defaultValue={currentModel}
key={currentModel}
placeholder="e.g., gpt-4o, claude-3-5-sonnet-20241002"
className="flex-1 px-4 py-2.5 rounded-xl text-sm"
style={{
@@ -371,15 +377,15 @@ function ModelInput({
color: 'var(--admin-text-primary)',
}}
/>
<button
<button type="button"
onClick={handleSave}
disabled={saving || !customModel.trim()}
disabled={saving}
className="px-5 py-2.5 rounded-xl text-sm font-semibold text-white transition-all"
style={{
background: 'linear-gradient(135deg, #ca7543 0%, #d4865a 100%)',
boxShadow: '0 2px 8px rgba(202, 117, 67, 0.25)',
opacity: (saving || !customModel.trim()) ? 0.5 : 1,
cursor: (saving || !customModel.trim()) ? 'not-allowed' : 'pointer',
opacity: saving ? 0.5 : 1,
cursor: saving ? 'not-allowed' : 'pointer',
}}
>
{saving ? "..." : saved ? "✓" : "Save"}
@@ -421,7 +427,7 @@ function ProviderSelector({
</div>
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
{PROVIDERS.map((provider) => (
<button
<button type="button"
key={provider.id}
onClick={() => handleSelect(provider.id)}
className="p-4 rounded-xl text-center transition-all"
@@ -515,7 +521,7 @@ function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName
<div className="space-y-4">
<div>
<label htmlFor="ai-cw-topic" className="block text-sm font-medium mb-1" style={labelStyle}>What do you want to communicate?</label>
<textarea
<textarea aria-label="., 'Remind Customers About The New Sweet Corn Season Starting Next Week, Emphasize Freshness And Local Delivery'"
id="ai-cw-topic"
value={topic}
onChange={(e) => setTopic(e.target.value)}
@@ -525,7 +531,7 @@ function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName
style={textareaStyle}
/>
</div>
<button
<button type="button"
onClick={handleGenerate}
disabled={loading || !topic.trim()}
className={btnPrimaryClass}
@@ -541,7 +547,7 @@ function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName
{results.length > 0 && (
<div className="space-y-3">
{results.map((idea, i) => (
<AdminCard key={i} className="p-4">
<AdminCard key={`idea-${i}-${idea.subject}`} className="p-4">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-accent)' }}>Idea {i + 1}</p>
<p className="text-sm font-medium mb-1" style={{ color: 'var(--admin-text-primary)' }}><span style={{ color: 'var(--admin-text-muted)' }}>Subject:</span> {idea.subject}</p>
<p className="text-sm mt-2 whitespace-pre-line" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{idea.body}</p>
@@ -593,22 +599,22 @@ function ProductWriterTool({ brandId }: { brandId: string }) {
<label htmlFor="ai-pw-product-name" className="block text-sm font-medium mb-1" style={labelStyle}>
Product Name <span className="text-red-400 ml-0.5" aria-hidden="true">*</span>
</label>
<input id="ai-pw-product-name" type="text" value={productName} onChange={(e) => setProductName(e.target.value)} placeholder="Sweet Corn" required aria-required="true" className={inputBaseClass} style={inputStyle} />
<input aria-label="Sweet Corn" id="ai-pw-product-name" type="text" value={productName} onChange={(e) => setProductName(e.target.value)} placeholder="Sweet Corn" required aria-required="true" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label htmlFor="ai-pw-category" className="block text-sm font-medium mb-1" style={labelStyle}>Category</label>
<input id="ai-pw-category" type="text" value={category} onChange={(e) => setCategory(e.target.value)} placeholder="Vegetables" className={inputBaseClass} style={inputStyle} />
<input aria-label="Vegetables" id="ai-pw-category" type="text" value={category} onChange={(e) => setCategory(e.target.value)} placeholder="Vegetables" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label htmlFor="ai-pw-price" className="block text-sm font-medium mb-1" style={labelStyle}>Price</label>
<input id="ai-pw-price" type="text" value={price} onChange={(e) => setPrice(e.target.value)} placeholder="$4.50" className={inputBaseClass} style={inputStyle} />
<input aria-label="$4.50" id="ai-pw-price" type="text" value={price} onChange={(e) => setPrice(e.target.value)} placeholder="$4.50" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label htmlFor="ai-pw-unit" className="block text-sm font-medium mb-1" style={labelStyle}>Unit</label>
<input id="ai-pw-unit" type="text" value={unit} onChange={(e) => setUnit(e.target.value)} placeholder="per dozen" className={inputBaseClass} style={inputStyle} />
<input aria-label="Per Dozen" id="ai-pw-unit" type="text" value={unit} onChange={(e) => setUnit(e.target.value)} placeholder="per dozen" className={inputBaseClass} style={inputStyle} />
</div>
</div>
<button
<button type="button"
onClick={handleGenerate}
disabled={loading || !productName.trim()}
className={btnPrimaryClass}
@@ -698,7 +704,7 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
<div className="grid grid-cols-2 gap-4">
<div>
<label htmlFor="ai-rep-type" className="block text-sm font-medium mb-1" style={labelStyle}>Report Type</label>
<select
<select aria-label="Ai Rep Type"
id="ai-rep-type"
value={reportType}
onChange={(e) => setReportType(e.target.value)}
@@ -712,7 +718,7 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
</div>
<div>
<label htmlFor="ai-rep-range" className="block text-sm font-medium mb-1" style={labelStyle}>Date Range</label>
<input
<input aria-label="., Last 7 Days, March 2026"
id="ai-rep-range"
type="text"
value={dateRange}
@@ -726,7 +732,7 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for business judgment.
</div>
<button
<button type="button"
onClick={handleExplain}
disabled={loading}
className={btnPrimaryClass}
@@ -744,8 +750,8 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Key Insights</p>
<ul className="space-y-2">
{result.keyInsights.map((insight, i) => (
<li key={i} className="flex items-start gap-2 text-sm" style={{ color: 'var(--admin-text-secondary)' }}>
{result.keyInsights.map((insight) => (
<li key={insight} className="flex items-start gap-2 text-sm" style={{ color: 'var(--admin-text-secondary)' }}>
<span style={{ color: 'var(--admin-accent)' }}></span>
<span>{insight}</span>
</li>
@@ -755,15 +761,15 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
<AdminCard className="p-5">
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: 'var(--admin-text-muted)' }}>Suggested Actions</p>
<ul className="space-y-2">
{result.suggestedActions.map((action, i) => (
<li key={i} className="flex items-start gap-2 text-sm" style={{ color: 'var(--admin-text-secondary)' }}>
{result.suggestedActions.map((action) => (
<li key={action} className="flex items-start gap-2 text-sm" style={{ color: 'var(--admin-text-secondary)' }}>
<span style={{ color: 'var(--admin-success)' }}></span>
<span>{action}</span>
</li>
))}
</ul>
</AdminCard>
<button
<button type="button"
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
@@ -864,7 +870,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<label htmlFor="ai-pa-product-name" className="block text-sm font-medium mb-1" style={labelStyle}>
Product Name <span className="text-red-400 ml-0.5" aria-hidden="true">*</span>
</label>
<input
<input aria-label="Sweet Corn"
id="ai-pa-product-name"
type="text"
value={productName}
@@ -881,11 +887,11 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Price Tiers</label>
<button onClick={addTier} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Tier</button>
<button type="button" onClick={addTier} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Tier</button>
</div>
{priceTiers.map((tier, i) => (
<div key={i} className="flex items-center gap-2">
<input
<div key={`tier-${i}-${tier.tier}-${tier.price}`} className="flex items-center gap-2">
<input aria-label="., Wholesale"
type="text"
value={tier.tier}
onChange={(e) => updateTier(i, "tier", e.target.value)}
@@ -895,7 +901,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
/>
<div className="relative flex-1">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm" style={{ color: 'var(--admin-text-muted)' }}>$</span>
<input
<input aria-label="0.00"
type="text"
value={tier.price}
onChange={(e) => updateTier(i, "price", e.target.value)}
@@ -905,7 +911,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
/>
</div>
{priceTiers.length > 1 && (
<button onClick={() => removeTier(i)} className="text-xs px-2 transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
<button type="button" onClick={() => removeTier(i)} className="text-xs px-2 transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
)}
</div>
))}
@@ -915,7 +921,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Historical Sales</label>
<button onClick={addSale} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
<button type="button" onClick={addSale} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
</div>
<AdminCard className="divide-y" style={{ border: '1px solid var(--admin-border)' }}>
<div className="grid grid-cols-4 gap-2 px-3 py-2">
@@ -925,8 +931,8 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<span />
</div>
{historicalSales.map((sale, i) => (
<div key={i} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
<input
<div key={`sale-${i}-${sale.date}`} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
<input aria-label="2026 04 01"
type="text"
value={sale.date}
onChange={(e) => updateSale(i, "date", e.target.value)}
@@ -934,7 +940,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
className="rounded-lg border px-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
<input
<input aria-label="120"
type="text"
value={sale.units_sold}
onChange={(e) => updateSale(i, "units_sold", e.target.value)}
@@ -944,7 +950,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
/>
<div className="relative">
<span className="absolute left-2 top-1/2 -translate-y-1/2 text-xs" style={{ color: 'var(--admin-text-muted)' }}>$</span>
<input
<input aria-label="540"
type="text"
value={sale.revenue}
onChange={(e) => updateSale(i, "revenue", e.target.value)}
@@ -954,7 +960,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
/>
</div>
{historicalSales.length > 1 && (
<button onClick={() => removeSale(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
<button type="button" onClick={() => removeSale(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
)}
</div>
))}
@@ -963,7 +969,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for business judgment.
</div>
<button
<button type="button"
onClick={handleAnalyze}
disabled={loading || !productName.trim()}
className={btnPrimaryClass}
@@ -983,7 +989,7 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<p className="text-sm" style={{ color: 'var(--admin-text-secondary)', lineHeight: 1.6 }}>{result.currentState}</p>
</AdminCard>
{result.recommendations.map((rec, i) => (
<AdminCard key={i} className="p-5">
<AdminCard key={`rec-${i}-${rec.productName}-${rec.direction}`} className="p-5">
<div className="flex items-center justify-between mb-2">
<p className="text-sm font-semibold" style={{ color: 'var(--admin-text-primary)' }}>{rec.productName}</p>
<span
@@ -1011,8 +1017,8 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<AdminCard className="p-5" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Opportunities</p>
<ul className="space-y-1">
{result.opportunities.map((opp, i) => (
<li key={i} className="text-sm" style={{ color: '#059669' }}> {opp}</li>
{result.opportunities.map((opp) => (
<li key={opp} className="text-sm" style={{ color: '#059669' }}> {opp}</li>
))}
</ul>
</AdminCard>
@@ -1021,13 +1027,13 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
<AdminCard className="p-5" style={{ backgroundColor: 'rgba(171, 162, 120, 0.08)', border: '1px solid rgba(171, 162, 120, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#92781e' }}>Warnings</p>
<ul className="space-y-1">
{result.warnings.map((warn, i) => (
<li key={i} className="text-sm" style={{ color: '#a6953f' }}> {warn}</li>
{result.warnings.map((warn) => (
<li key={warn} className="text-sm" style={{ color: '#a6953f' }}> {warn}</li>
))}
</ul>
</AdminCard>
)}
<button
<button type="button"
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
@@ -1100,25 +1106,25 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Stop Name *</label>
<input type="text" value={stopName} onChange={(e) => setStopName(e.target.value)} placeholder="Downtown Farmers Market" className={inputBaseClass} style={inputStyle} />
<input aria-label="Downtown Farmers Market" type="text" value={stopName} onChange={(e) => setStopName(e.target.value)} placeholder="Downtown Farmers Market" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Stop Date</label>
<input type="text" value={stopDate} onChange={(e) => setStopDate(e.target.value)} placeholder="2026-06-15" className={inputBaseClass} style={inputStyle} />
<input aria-label="2026 06 15" type="text" value={stopDate} onChange={(e) => setStopDate(e.target.value)} placeholder="2026-06-15" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>City</label>
<input type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder="Greeley" className={inputBaseClass} style={inputStyle} />
<input aria-label="Greeley" type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder="Greeley" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Customer Count</label>
<input type="text" value={customerCount} onChange={(e) => setCustomerCount(e.target.value)} placeholder="42" className={inputBaseClass} style={inputStyle} />
<input aria-label="42" type="text" value={customerCount} onChange={(e) => setCustomerCount(e.target.value)} placeholder="42" className={inputBaseClass} style={inputStyle} />
</div>
</div>
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for business judgment.
</div>
<button
<button type="button"
onClick={handleAnalyze}
disabled={loading || !stopName.trim()}
className={btnPrimaryClass}
@@ -1140,7 +1146,7 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
<AdminCard className="p-5" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Recommended Subject Line</p>
<p className="text-base font-semibold" style={{ color: 'var(--admin-text-primary)' }}>{result.subjectLine}</p>
<button onClick={() => copyToClipboard(result.subjectLine)} className="mt-2 text-xs flex items-center gap-1 transition-colors" style={{ color: 'var(--admin-accent)' }}>
<button type="button" onClick={() => copyToClipboard(result.subjectLine)} className="mt-2 text-xs flex items-center gap-1 transition-colors" style={{ color: 'var(--admin-accent)' }}>
Copy
</button>
</AdminCard>
@@ -1159,7 +1165,7 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
</span>
</AdminCard>
{result.contentAngles.map((a, i) => (
<AdminCard key={i} className="p-4">
<AdminCard key={`angle-${i}-${a.angle}`} className="p-4">
<p className="text-sm font-semibold mb-1" style={{ color: 'var(--admin-text-primary)' }}>{a.angle}</p>
<p className="text-xs" style={{ color: 'var(--admin-text-muted)' }}>{a.reasoning}</p>
</AdminCard>
@@ -1167,8 +1173,8 @@ function StopBlastAdvisorTool({ brandId }: { brandId: string }) {
{result.warnings.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(171, 162, 120, 0.08)', border: '1px solid rgba(171, 162, 120, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#92781e' }}>Warnings</p>
{result.warnings.map((w, i) => (
<p key={i} className="text-xs" style={{ color: '#a6953f' }}> {w}</p>
{result.warnings.map((w) => (
<p key={w} className="text-xs" style={{ color: '#a6953f' }}> {w}</p>
))}
</AdminCard>
)}
@@ -1230,7 +1236,7 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) {
</p>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Ask about your customers</label>
<textarea
<textarea aria-label="., 'Which Customers Haven't Ordered In 45 Days?'"
value={query}
onChange={(e) => setQuery(e.target.value)}
rows={2}
@@ -1241,7 +1247,7 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) {
</div>
<div className="flex flex-wrap gap-2">
{exampleQueries.map((q) => (
<button
<button type="button"
key={q}
onClick={() => { setQuery(q); handleAnalyze(q); }}
className="rounded-full px-3 py-1 text-xs transition-all"
@@ -1258,7 +1264,7 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) {
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for business judgment.
</div>
<button
<button type="button"
onClick={() => handleAnalyze()}
disabled={loading || !query.trim()}
className={btnPrimaryClass}
@@ -1300,13 +1306,16 @@ function CustomerInsightsTool({ brandId }: { brandId: string }) {
</tr>
</thead>
<tbody>
{result.results.map((row, i) => (
<tr key={i} style={{ borderBottom: '1px solid var(--admin-border-light)' }}>
{Object.values(row).map((v, j) => (
<td key={j} className="px-2 py-1.5" style={{ color: 'var(--admin-text-secondary)' }}>{String(v ?? "—")}</td>
{result.results.map((row) => {
const rowKey = Object.values(row).slice(0, 2).map(v => String(v ?? "")).join("-");
return (
<tr key={rowKey} style={{ borderBottom: '1px solid var(--admin-border-light)' }}>
{Object.entries(row).map(([colKey, v]) => (
<td key={colKey} className="px-2 py-1.5" style={{ color: 'var(--admin-text-secondary)' }}>{String(v ?? "—")}</td>
))}
</tr>
))}
);
})}
</tbody>
</table>
</div>
@@ -1388,7 +1397,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
</p>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Start Location</label>
<input
<input aria-label="Warehouse, Greeley CO"
type="text"
value={startLocation}
onChange={(e) => setStartLocation(e.target.value)}
@@ -1401,7 +1410,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
<div className="space-y-3">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Stops</label>
<button
<button type="button"
onClick={addStop}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-accent)' }}
@@ -1410,11 +1419,11 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
</button>
</div>
{stops.map((stop, i) => (
<AdminCard key={i} className="p-4 space-y-3">
<AdminCard key={`stop-${i}-${stop.name}-${stop.city}-${stop.state}`} className="p-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-xs font-semibold uppercase tracking-wider" style={{ color: 'var(--admin-text-muted)' }}>Stop {i + 1}</span>
{stops.length > 2 && (
<button onClick={() => removeStop(i)} className="text-xs transition-colors" style={{ color: 'var(--admin-danger)' }}>
<button type="button" onClick={() => removeStop(i)} className="text-xs transition-colors" style={{ color: 'var(--admin-danger)' }}>
Remove
</button>
)}
@@ -1422,7 +1431,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>Name *</label>
<input
<input aria-label="Farmers Market"
type="text"
value={stop.name}
onChange={(e) => updateStop(i, "name", e.target.value)}
@@ -1433,7 +1442,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
</div>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>City *</label>
<input
<input aria-label="Greeley"
type="text"
value={stop.city}
onChange={(e) => updateStop(i, "city", e.target.value)}
@@ -1444,7 +1453,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
</div>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>State *</label>
<input
<input aria-label="CO"
type="text"
value={stop.state}
onChange={(e) => updateStop(i, "state", e.target.value)}
@@ -1455,7 +1464,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
</div>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>Address</label>
<input
<input aria-label="123 Main St"
type="text"
value={stop.address}
onChange={(e) => updateStop(i, "address", e.target.value)}
@@ -1467,7 +1476,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
</div>
<div>
<label className="block text-xs mb-1" style={{ color: 'var(--admin-text-muted)' }}>Time Window</label>
<input
<input aria-label="8am12pm"
type="text"
value={stop.time_window}
onChange={(e) => updateStop(i, "time_window", e.target.value)}
@@ -1482,7 +1491,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated suggestions review before use. Not a substitute for professional routing software.
</div>
<button
<button type="button"
onClick={handleOptimize}
disabled={loading || validStops.length < 2}
className={btnPrimaryClass}
@@ -1510,7 +1519,7 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Optimized Sequence</p>
{result.optimizedSequence.map((s, i) => (
<div key={i} className="flex items-start gap-3 py-2" style={{ borderBottom: i < result.optimizedSequence.length - 1 ? '1px solid var(--admin-border-light)' : 'none' }}>
<div key={`seq-${i}-${s.position}-${s.stopName}`} className="flex items-start gap-3 py-2" style={{ borderBottom: i < result.optimizedSequence.length - 1 ? '1px solid var(--admin-border-light)' : 'none' }}>
<span
className="flex items-center justify-center w-6 h-6 rounded-full text-xs font-bold text-white flex-shrink-0"
style={{ backgroundColor: 'var(--admin-accent)' }}
@@ -1528,16 +1537,16 @@ function RouteOptimizerTool({ brandId }: { brandId: string }) {
{result.suggestions.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(16, 185, 129, 0.05)', border: '1px solid rgba(16, 185, 129, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#047857' }}>Suggestions</p>
{result.suggestions.map((s, i) => <p key={i} className="text-sm" style={{ color: '#059669' }}> {s}</p>)}
{result.suggestions.map((s) => <p key={s} className="text-sm" style={{ color: '#059669' }}> {s}</p>)}
</AdminCard>
)}
{result.warnings.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(171, 162, 120, 0.08)', border: '1px solid rgba(171, 162, 120, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#92781e' }}>Warnings</p>
{result.warnings.map((w, i) => <p key={i} className="text-sm" style={{ color: '#a6953f' }}> {w}</p>)}
{result.warnings.map((w) => <p key={w} className="text-sm" style={{ color: '#a6953f' }}> {w}</p>)}
</AdminCard>
)}
<button
<button type="button"
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
@@ -1623,11 +1632,11 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Product Name *</label>
<input type="text" value={productName} onChange={(e) => setProductName(e.target.value)} placeholder="Sweet Corn" className={inputBaseClass} style={inputStyle} />
<input aria-label="Sweet Corn" type="text" value={productName} onChange={(e) => setProductName(e.target.value)} placeholder="Sweet Corn" className={inputBaseClass} style={inputStyle} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Stop Name</label>
<input type="text" value={stopName} onChange={(e) => setStopName(e.target.value)} placeholder="Downtown Farmers Market" className={inputBaseClass} style={inputStyle} />
<input aria-label="Downtown Farmers Market" type="text" value={stopName} onChange={(e) => setStopName(e.target.value)} placeholder="Downtown Farmers Market" className={inputBaseClass} style={inputStyle} />
</div>
</div>
@@ -1635,7 +1644,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="block text-sm font-medium" style={labelStyle}>Historical Sales</label>
<button onClick={addRow} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
<button type="button" onClick={addRow} className="text-xs transition-colors" style={{ color: 'var(--admin-accent)' }}>+ Add Row</button>
</div>
<AdminCard className="divide-y" style={{ border: '1px solid var(--admin-border)' }}>
<div className="grid grid-cols-4 gap-2 px-3 py-2">
@@ -1645,8 +1654,8 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
<span />
</div>
{historicalData.map((row, i) => (
<div key={i} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
<input
<div key={`hist-${i}-${row.date}-${row.stop}`} className="grid grid-cols-4 gap-2 px-3 py-2 items-center">
<input aria-label="2026 04 01"
type="text"
value={row.date}
onChange={(e) => updateRow(i, "date", e.target.value)}
@@ -1654,7 +1663,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
className="rounded-lg border px-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
<input
<input aria-label="120"
type="text"
value={row.quantity_sold}
onChange={(e) => updateRow(i, "quantity_sold", e.target.value)}
@@ -1662,7 +1671,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
className="rounded-lg border px-2 py-1.5 text-xs"
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
<input
<input aria-label="Farmers Market"
type="text"
value={row.stop}
onChange={(e) => updateRow(i, "stop", e.target.value)}
@@ -1671,7 +1680,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
style={{ borderColor: 'var(--admin-border)', backgroundColor: 'var(--admin-bg)', color: 'var(--admin-text-primary)' }}
/>
{historicalData.length > 1 && (
<button onClick={() => removeRow(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
<button type="button" onClick={() => removeRow(i)} className="text-xs justify-self-end transition-colors" style={{ color: 'var(--admin-danger)' }}></button>
)}
</div>
))}
@@ -1680,7 +1689,7 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
<div className="rounded-lg p-3 text-xs" style={{ backgroundColor: 'rgba(171, 162, 120, 0.1)', border: '1px solid rgba(171, 162, 120, 0.2)', color: '#92781e' }}>
AI-generated forecasts review before use. Not a substitute for professional supply chain planning.
</div>
<button
<button type="button"
onClick={handleAnalyze}
disabled={loading || !productName.trim()}
className={btnPrimaryClass}
@@ -1730,16 +1739,16 @@ function DemandForecastTool({ brandId }: { brandId: string }) {
{result.seasonalFactors.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(59, 130, 246, 0.05)', border: '1px solid rgba(59, 130, 246, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#2563eb' }}>Seasonal Factors</p>
{result.seasonalFactors.map((f, i) => <p key={i} className="text-sm" style={{ color: '#3b82f6' }}> {f}</p>)}
{result.seasonalFactors.map((f) => <p key={f} className="text-sm" style={{ color: '#3b82f6' }}> {f}</p>)}
</AdminCard>
)}
{result.riskFlags.length > 0 && (
<AdminCard className="p-4" style={{ backgroundColor: 'rgba(171, 162, 120, 0.08)', border: '1px solid rgba(171, 162, 120, 0.2)' }}>
<p className="text-xs font-semibold uppercase tracking-wider mb-2" style={{ color: '#92781e' }}>Risk Flags</p>
{result.riskFlags.map((r, i) => <p key={i} className="text-sm" style={{ color: '#a6953f' }}> {r}</p>)}
{result.riskFlags.map((r) => <p key={r} className="text-sm" style={{ color: '#a6953f' }}> {r}</p>)}
</AdminCard>
)}
<button
<button type="button"
onClick={() => copyToClipboard(JSON.stringify(result, null, 2))}
className="text-xs flex items-center gap-1 transition-colors"
style={{ color: 'var(--admin-text-muted)' }}
@@ -1770,7 +1779,7 @@ function ToolModal({ tool, brandId, brandName, onClose }: ModalProps) {
<span className="text-2xl">{tool.icon}</span>
<h2 className="text-lg font-bold" style={{ color: 'var(--admin-text-primary)' }}>{tool.title}</h2>
</div>
<button onClick={onClose} className="p-2 transition-colors" style={{ color: 'var(--admin-text-muted)' }}>
<button type="button" onClick={onClose} className="p-2 transition-colors" style={{ color: 'var(--admin-text-muted)' }}>
<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>
@@ -1803,8 +1812,11 @@ export default function AIsettingsClient({
}: Props) {
const [activeTool, setActiveTool] = useState<AITool | null>(null);
const [activeModule, setActiveModule] = useState<string | null>(null);
const [selectedProvider, setSelectedProvider] = useState<Provider>(provider);
const [currentModel, setCurrentModel] = useState(model);
// selectedProvider/currentModel are derived directly from props. We use a
// `key` on the child components so any prop change fully remounts them and
// their internal useState(initialProp) re-initializes — no stale copy.
const selectedProvider = provider;
const currentModel = model;
const filteredTools = activeModule ? AI_TOOLS.filter((t) => t.module === activeModule) : AI_TOOLS;
const modules = [...new Set(AI_TOOLS.map((t) => t.module))];
@@ -1846,19 +1858,27 @@ export default function AIsettingsClient({
currentProvider={selectedProvider}
currentModel={currentModel}
brandId={brandId}
onSelect={setSelectedProvider}
onSelect={() => {
// Local state is derived from `provider` prop. After a server
// refresh the new provider flows in via the parent re-render.
}}
/>
{/* Model Input */}
<ModelInput
key={currentModel}
currentModel={currentModel}
brandId={brandId}
onChange={setCurrentModel}
onChange={() => {
// Local state is derived from `model` prop. `key={currentModel}`
// remounts this component so its internal useState(currentModel)
// re-syncs to the new value.
}}
/>
{/* Module filter */}
<div className="admin-filter-tabs mb-6">
<button
<button type="button"
onClick={() => setActiveModule(null)}
className={`admin-filter-tab ${activeModule === null ? 'admin-filter-tab--active' : ''}`}
>
@@ -1867,7 +1887,7 @@ export default function AIsettingsClient({
{modules.map((m) => {
const count = AI_TOOLS.filter((t) => t.module === m).length;
return (
<button
<button type="button"
key={m}
onClick={() => setActiveModule(activeModule === m ? null : m)}
className={`admin-filter-tab ${activeModule === m ? 'admin-filter-tab--active' : ''}`}