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:
@@ -0,0 +1,82 @@
|
||||
-- 203_plan_usage_active_products.sql
|
||||
-- Reconcile plan usage product count with the dashboard "Active Products" stat.
|
||||
--
|
||||
-- Context:
|
||||
-- Migration 091's get_brand_plan_info counted `products` using
|
||||
-- deleted_at IS NULL
|
||||
-- while the dashboard's getDashboardStats server action counted
|
||||
-- active = true
|
||||
-- These two filters can disagree (a product can be `active=false` but
|
||||
-- not soft-deleted, or `active=true` and soft-deleted in some flows),
|
||||
-- producing "Active Products 1" in the dashboard stats while the
|
||||
-- billing/usage bar said "Products 0/25".
|
||||
--
|
||||
-- This migration updates get_brand_plan_info to count products that are
|
||||
-- active AND not soft-deleted — the natural meaning of "products you sell
|
||||
-- that count against the plan limit". The same filter is also applied to
|
||||
-- the stop count (active AND not soft-deleted AND in the current month)
|
||||
-- to keep the usage semantics consistent across all three counters.
|
||||
--
|
||||
-- Uses CREATE OR REPLACE FUNCTION so re-running is safe.
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_brand_plan_info(p_brand_id UUID)
|
||||
RETURNS JSONB AS $$
|
||||
DECLARE
|
||||
v_result JSONB;
|
||||
v_user_count BIGINT;
|
||||
v_active_stops BIGINT;
|
||||
v_product_count BIGINT;
|
||||
v_brand RECORD;
|
||||
BEGIN
|
||||
SELECT plan_tier, max_users, max_stops_monthly, max_products, stripe_customer_id, name
|
||||
INTO v_brand
|
||||
FROM brands WHERE id = p_brand_id;
|
||||
|
||||
IF v_brand IS NULL THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- Count current usage with consistent semantics:
|
||||
-- users: not soft-deleted
|
||||
-- stops: active AND not soft-deleted AND in the current month
|
||||
-- products: active AND not soft-deleted (matches "Active Products"
|
||||
-- stat used in the admin dashboard, so plan usage and
|
||||
-- dashboard stats never disagree)
|
||||
SELECT COUNT(*) INTO v_user_count
|
||||
FROM admin_users
|
||||
WHERE brand_id = p_brand_id
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
SELECT COUNT(*) INTO v_active_stops
|
||||
FROM stops
|
||||
WHERE brand_id = p_brand_id
|
||||
AND active = true
|
||||
AND deleted_at IS NULL
|
||||
AND date >= date_trunc('month', now());
|
||||
|
||||
SELECT COUNT(*) INTO v_product_count
|
||||
FROM products
|
||||
WHERE brand_id = p_brand_id
|
||||
AND active = true
|
||||
AND deleted_at IS NULL;
|
||||
|
||||
SELECT jsonb_build_object(
|
||||
'plan_tier', v_brand.plan_tier,
|
||||
'max_users', v_brand.max_users,
|
||||
'max_stops_monthly', v_brand.max_stops_monthly,
|
||||
'max_products', v_brand.max_products,
|
||||
'stripe_customer_id', v_brand.stripe_customer_id,
|
||||
'brand_name', v_brand.name,
|
||||
'usage', jsonb_build_object(
|
||||
'users', v_user_count,
|
||||
'stops_this_month', v_active_stops,
|
||||
'products', v_product_count
|
||||
)
|
||||
) INTO v_result;
|
||||
|
||||
RETURN v_result;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
||||
|
||||
-- Reload PostgREST schema cache so the change is picked up immediately.
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
Reference in New Issue
Block a user