fix(buyer/billing/comms/a11y): Codex review pass round 2

Tuxedo buyer path (subagent 2):
- src/app/tuxedo/page.tsx: remove duplicate CinematicShowcase render
- src/components/storefront/CinematicShowcase.tsx: wire up useCart, Add to Cart button, brand-aware fulfillment
- src/app/tuxedo/stops/TuxedoStopsList.tsx: improved empty state with calendar icon + CTAs
- src/app/cart/CartClient.tsx: guard against empty cart checkout; 'Cart is Empty' state

Billing reconciliation (subagent 3):
- src/actions/billing/billing-overview.ts: NEW — single source of truth
- src/app/admin/settings/billing/page.tsx: use getBillingOverview
- src/app/admin/settings/billing/BillingClientPage.tsx: rewritten to consume BillingOverview (status pill, addons state, removable flags, derived invoice amounts, usage footer)
- src/app/admin/page.tsx: use getBillingOverview (aligns dashboard with billing)
- src/components/admin/DashboardClient.tsx: 'Active Products' now reads from getBillingOverview
- supabase/migrations/203_plan_usage_active_products.sql: get_brand_plan_info counts products as active=true AND deleted_at IS NULL

Harvest Reach dedup + audience preview (manual, subagent 4 didn't complete):
- src/components/admin/CommunicationsPage.tsx: add initialTab prop
- src/app/admin/communications/compose/page.tsx: now renders with initialTab='compose' (single compose experience, no duplicate edit panel)
- src/components/admin/HarvestReach/CampaignComposerPage.tsx: always-visible audience preview panel (count + sample emails), loads via previewCampaignAudience action

Layout/content consistency + a11y sweep (subagent 5):
- src/components/layout/SiteHeader.tsx: Admin link only shows for authenticated admin users
- src/components/Providers.tsx: suppress public SiteHeader/Footer for /admin, /cart, /checkout, /wholesale, /water (fixes duplicate headers)
- src/app/contact/ContactClientPage.tsx: Phone/Email now use tel:/mailto: links; dynamic year
- src/app/blog/page.tsx, changelog, privacy-policy, roadmap, security, terms-and-conditions, waitlist: dynamic year
- src/app/admin/wholesale/WholesaleClient.tsx: proper htmlFor/id, type=email/tel, autoComplete
- src/app/admin/settings/ai/AIClient.tsx: proper htmlFor/id, required + aria-required
- src/app/admin/settings/integrations/IntegrationsClient.tsx: only mask secret fields; add required + aria-required + CredentialField.required type
- src/app/admin/water-log/headgates/HeadgatesManager.tsx: htmlFor/id, aria-required
- src/components/admin/CreateUserModal.tsx: htmlFor/id, required, aria-required, aria-describedby, autoComplete
- src/app/admin/me/AdminMeClient.tsx, products/import, sales/import, water-log/settings, login, brands, tuxedo: a11y polish (ids/required/aria)
This commit is contained in:
2026-06-03 16:39:19 +00:00
parent 03ae372509
commit 0245aa29cc
34 changed files with 1122 additions and 295 deletions
+22 -12
View File
@@ -514,8 +514,9 @@ function CampaignWriterTool({ brandId, brandName }: { brandId: string; brandName
return (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>What do you want to communicate?</label>
<label htmlFor="ai-cw-topic" className="block text-sm font-medium mb-1" style={labelStyle}>What do you want to communicate?</label>
<textarea
id="ai-cw-topic"
value={topic}
onChange={(e) => setTopic(e.target.value)}
rows={3}
@@ -589,20 +590,22 @@ function ProductWriterTool({ brandId }: { brandId: string }) {
<div className="space-y-4">
<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} />
<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} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Category</label>
<input type="text" value={category} onChange={(e) => setCategory(e.target.value)} placeholder="Vegetables" className={inputBaseClass} style={inputStyle} />
<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} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Price</label>
<input type="text" value={price} onChange={(e) => setPrice(e.target.value)} placeholder="$4.50" className={inputBaseClass} style={inputStyle} />
<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} />
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Unit</label>
<input type="text" value={unit} onChange={(e) => setUnit(e.target.value)} placeholder="per dozen" className={inputBaseClass} style={inputStyle} />
<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} />
</div>
</div>
<button
@@ -694,8 +697,9 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
</p>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Report Type</label>
<label htmlFor="ai-rep-type" className="block text-sm font-medium mb-1" style={labelStyle}>Report Type</label>
<select
id="ai-rep-type"
value={reportType}
onChange={(e) => setReportType(e.target.value)}
className={inputBaseClass}
@@ -707,8 +711,9 @@ function ReportExplainerTool({ brandId }: { brandId: string }) {
</select>
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Date Range</label>
<label htmlFor="ai-rep-range" className="block text-sm font-medium mb-1" style={labelStyle}>Date Range</label>
<input
id="ai-rep-range"
type="text"
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
@@ -856,12 +861,17 @@ function PricingAdvisorTool({ brandId }: { brandId: string }) {
Enter a product name and optional price tiers or historical sales data for AI-powered pricing recommendations.
</p>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Product Name *</label>
<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
id="ai-pa-product-name"
type="text"
value={productName}
onChange={(e) => setProductName(e.target.value)}
placeholder="Sweet Corn"
required
aria-required="true"
className={inputBaseClass}
style={inputStyle}
/>