Skip to content

chore: Phase 5 code review - tenant admin pages#12

Merged
AutomatosAI merged 1 commit intomainfrom
review/phase5-tenant-admin
Jan 13, 2026
Merged

chore: Phase 5 code review - tenant admin pages#12
AutomatosAI merged 1 commit intomainfrom
review/phase5-tenant-admin

Conversation

@AutomatosAI
Copy link
Copy Markdown
Owner

@AutomatosAI AutomatosAI commented Jan 13, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced multi-step onboarding wizard with four sequential steps for streamlined account setup
    • Enhanced customer details page with improved layout and access controls for tenant administrators
    • Restructured SEO management interface with tabbed organization for products, posts, and pages
  • Improvements

    • Audit logs now refresh automatically when filters or pagination settings change
    • Consistent formatting updates across authentication flows and admin interfaces

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 13, 2026

📝 Walkthrough

Walkthrough

This PR standardizes code formatting across the application (single to double quotes, spacing, indentation) and introduces functional enhancements: a new forgot-password submission flow, multi-step onboarding wizard, server-side auth/access controls for customer pages, and improved UI structuring in tenant-admin sections.

Changes

Cohort / File(s) Summary
Auth - Password Recovery
nextjs_space/app/auth/forgot-password/page.tsx, nextjs_space/app/auth/reset-password/[token]/page.tsx
Introduces client-side submit handlers and password reset flow with validation UI. Forgot-password now calls /api/auth/reset-password with email. Reset-password adds early validation error display and success redirect UI after password update.
Auth - Core
nextjs_space/app/auth/login/page.tsx, nextjs_space/app/auth/signup/page.tsx
Standardized quote styles and minor text formatting in login and signup pages. No functional changes to authentication logic.
Onboarding
nextjs_space/app/onboarding/onboarding-form.tsx, nextjs_space/app/onboarding/page.tsx
Refactored onboarding from single-pass form to multi-step wizard with four steps (business info, account setup, verification, template selection). Added step validation, progress indicators, and subdomain generation.
Tenant Admin - Dashboard & Layout
nextjs_space/app/tenant-admin/page.tsx, nextjs_space/app/tenant-admin/layout.tsx
Minor formatting updates and added store-visit UI block. Authentication and redirect logic preserved.
Tenant Admin - Customers
nextjs_space/app/tenant-admin/customers/page.tsx, nextjs_space/app/tenant-admin/customers/customers-table.tsx, nextjs_space/app/tenant-admin/customers/[id]/page.tsx, nextjs_space/app/tenant-admin/customers/[id]/customer-actions.tsx, nextjs_space/app/tenant-admin/customers/[id]/customer-edit-form.tsx
Customer detail page now enforces server-side session checks and tenant-based access control; introduced Customer interface export. Added notFound() guard for unauthorized access. Minor formatting updates across action and edit forms.
Tenant Admin - Orders
nextjs_space/app/tenant-admin/orders/page.tsx, nextjs_space/app/tenant-admin/orders/orders-table.tsx, nextjs_space/app/tenant-admin/orders/[id]/packing-slip/page.tsx
Quote standardization and minor formatting adjustments. No changes to order logic, filtering, or bulk action handling.
Tenant Admin - Products
nextjs_space/app/tenant-admin/products/page.tsx, nextjs_space/app/tenant-admin/products/products-table.tsx
Quote standardization and type definition formatting updates. No functional changes to product queries or table rendering.
Tenant Admin - Emails
nextjs_space/app/tenant-admin/emails/page.tsx, nextjs_space/app/tenant-admin/emails/[id]/page.tsx, nextjs_space/app/tenant-admin/emails/[id]/client.tsx, nextjs_space/app/tenant-admin/emails/new/page.tsx, nextjs_space/app/tenant-admin/emails/new/client.tsx
Formatting and quote updates across email template pages. handleSave logic and API calls preserved.
Tenant Admin - Branding
nextjs_space/app/tenant-admin/branding/page.tsx, nextjs_space/app/tenant-admin/branding/branding-form.tsx, nextjs_space/app/tenant-admin/branding/comprehensive-branding-form.tsx
Quote standardization, object property formatting, and minor function signature adjustments (trailing commas). No changes to form state or submission logic.
Tenant Admin - Templates
nextjs_space/app/tenant-admin/templates/page.tsx, nextjs_space/app/tenant-admin/templates/activate-button.tsx, nextjs_space/app/tenant-admin/templates/clone-button.tsx
Quote standardization and structural refactoring of activate and clone button components. Activation and cloning flows remain functionally identical.
Tenant Admin - The Wire (Articles)
nextjs_space/app/tenant-admin/the-wire/page.tsx, nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx, nextjs_space/app/tenant-admin/the-wire/new/page.tsx, nextjs_space/app/tenant-admin/the-wire/post-form.tsx, nextjs_space/app/tenant-admin/the-wire/posts-list.tsx
Quote standardization and minor JSX restructuring. Post-form now separates image upload button from main form submission. Publish/delete logic and data handling unchanged.
Tenant Admin - Analytics & Reporting
nextjs_space/app/tenant-admin/analytics/page.tsx, nextjs_space/app/tenant-admin/audit-logs/page.tsx
Quote standardization and type annotation updates. Audit logs now includes dependencies (page, actionFilter, entityFilter) to useEffect for reactive data re-fetching.
Tenant Admin - SEO & Settings
nextjs_space/app/tenant-admin/seo/page.tsx, nextjs_space/app/tenant-admin/seo/seo-page-client.tsx, nextjs_space/app/tenant-admin/settings/page.tsx, nextjs_space/app/tenant-admin/settings/settings-form.tsx
Quote standardization and SEO modal structure reorganization. Settings form maintains identical validation and submission flows.
Tenant Admin - Misc
nextjs_space/app/tenant-admin/cookie-settings/page.tsx, nextjs_space/app/tenant-admin/cookie-settings/settings-form.tsx, nextjs_space/app/tenant-admin/webhooks/page.tsx
Quote standardization and formatting adjustments only. No functional changes to cookie or webhook logic.

Sequence Diagrams

sequenceDiagram
    participant User
    participant Client as Forgot Password Page
    participant API as /api/auth/reset-password
    participant Backend as Auth Service

    User->>Client: Enter email, submit
    Client->>Client: handleSubmit triggered
    Client->>API: POST email (JSON)
    API->>Backend: Process reset request
    alt Success
        Backend-->>API: OK response
        API-->>Client: Success response
        Client->>Client: Set submitted = true
        Client->>User: Show success alert
    else Error
        Backend-->>API: Error response
        API-->>Client: Error details
        Client->>Client: Console error logged
        Client->>User: Show error alert
    end
Loading
sequenceDiagram
    participant User
    participant Wizard as Onboarding Wizard
    participant FormState as React State
    participant API as /api/onboarding
    participant Backend as Onboarding Service

    User->>Wizard: Start onboarding
    Wizard->>Wizard: Display Step 1 (Business Info)
    User->>Wizard: Enter business details
    Wizard->>FormState: validateStep(1)
    alt Step valid
        User->>Wizard: Click Next
        Wizard->>Wizard: Display Step 2 (Account)
    else Invalid
        Wizard->>User: Show error toast
    end
    
    Note over Wizard: Steps 2-3: Email/Password, NFT Verification
    
    User->>Wizard: Complete Step 4 (Template)
    Wizard->>FormState: handleSubmit()
    FormState->>API: POST complete onboarding data
    API->>Backend: Process registration
    Backend-->>API: Success confirmation
    API-->>FormState: Success response
    FormState->>User: Navigate to dashboard

Loading
sequenceDiagram
    participant Request as Incoming Request
    participant Page as Customer Detail Page
    participant Auth as getServerSession
    participant DB as Database/Prisma
    participant Response as HTTP Response

    Request->>Page: GET /customers/[id]
    Page->>Auth: Validate session & roles
    alt Unauthorized
        Auth-->>Response: Redirect to /auth/login
    else Authorized
        Page->>DB: Check tenant authorization
        DB-->>Page: Verify tenantId matches
        alt Access denied
            Page-->>Response: notFound()
        else Allowed
            Page->>DB: Fetch customer + counts
            DB-->>Page: Customer data
            Page-->>Response: Render customer detail page
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

codex


🐰 A rabbit hops through formatting's maze,
Double quotes now light the code's ways,
Steps bloom in onboarding's flow,
Auth guards stand strong, to and fro,
Ten admin pages renewed—what a craze! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'chore: Phase 5 code review - tenant admin pages' is partially related to the changeset but overly broad and vague. While tenant admin pages are indeed modified, the title doesn't convey the specific nature of the changes (formatting standardization, logic refactors, and feature additions across multiple pages). Consider a more specific title that describes the primary changes, such as 'chore: standardize code formatting and refactor tenant admin pages' or 'chore: Phase 5 - quote standardization and tenant admin refactors'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
nextjs_space/app/tenant-admin/audit-logs/page.tsx (2)

131-166: Selecting "All" sends action=all or entityType=all to the API, which may be unintended.

When a user selects "All Actions" or "All Entities", the filter state becomes "all". Since "all" is truthy, it gets appended to the query params (lines 76-77). This differs from the initial empty string state which correctly skips appending the parameter.

If the API doesn't explicitly handle action=all or entityType=all, this could return unexpected results or no results.

🐛 Proposed fix

Either use empty string for the "All" options:

                 <SelectContent>
-                  <SelectItem value="all">All Actions</SelectItem>
+                  <SelectItem value="">All Actions</SelectItem>

Or update the query param logic to skip "all":

-      if (actionFilter) params.append("action", actionFilter);
-      if (entityFilter) params.append("entityType", entityFilter);
+      if (actionFilter && actionFilter !== "all") params.append("action", actionFilter);
+      if (entityFilter && entityFilter !== "all") params.append("entityType", entityFilter);

61-66: Page number should reset to 1 when filters change.

When actionFilter or entityFilter changes, the useEffect re-fetches logs but keeps the current page number. If the user is on page 5 and applies a filter that yields only 2 pages of results, they may see empty results or an error.

🐛 Proposed fix

Reset page to 1 when filters change:

+  // Reset to page 1 when filters change
+  useEffect(() => {
+    setPagination((prev) => ({ ...prev, page: 1 }));
+  }, [actionFilter, entityFilter]);

   useEffect(() => {
     fetchLogs();
   }, [pagination.page, actionFilter, entityFilter]);

Or handle it inline in the filter change handlers:

-              <Select value={actionFilter} onValueChange={setActionFilter}>
+              <Select value={actionFilter} onValueChange={(value) => {
+                setActionFilter(value);
+                setPagination((prev) => ({ ...prev, page: 1 }));
+              }}>
nextjs_space/app/tenant-admin/branding/branding-form.tsx (1)

908-920: Hardcoded preview URL should use dynamic tenant URL.

The store preview link uses a hardcoded domain healingbuds.abacusai.app instead of deriving it from the tenant's subdomain or custom domain. This would show incorrect URLs for other tenants.

🔧 Suggested fix using tenant subdomain
 <a
-  href={`https://healingbuds.abacusai.app/store/${tenant.subdomain}`}
+  href={tenant.customDomain ? `https://${tenant.customDomain}` : `https://${tenant.subdomain}.yourdomain.com`}
   target="_blank"
   rel="noopener noreferrer"
   className="inline-flex items-center justify-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
 >

Alternatively, import and use the getTenantUrl utility function used in the tenant-admin page.

🤖 Fix all issues with AI agents
In @nextjs_space/app/onboarding/onboarding-form.tsx:
- Around line 408-485: The variable selectedTemplate declared in case 4 of the
switch is not block-scoped and should be confined to that case; wrap the entire
case 4 body in a block (i.e., add { ... } immediately after case 4) so the const
selectedTemplate = templates.find(...) and the JSX return are inside that block,
preventing the declaration from leaking into other cases; keep the return as-is
inside the new block.
- Around line 385-400: The United Kingdom option in the country <select> in
onboarding-form.tsx uses the non-standard value "UK"; change its value to the
ISO 3166-1 alpha-2 code "GB" (i.e., update the <option value="UK">… to <option
value="GB">…). After changing, search this component for any comparisons,
default values, or form state that reference "UK" and update them to "GB" to
keep behavior consistent with consultation-constants.ts and other components.

In @nextjs_space/app/tenant-admin/branding/page.tsx:
- Around line 1-8: The call to getServerSession() inside BrandingPage is missing
the authOptions parameter so custom session fields (e.g., session.user.id,
tenantId, role) are not populated; update the call to
getServerSession(authOptions) using the same authOptions export used elsewhere
(import or reference the existing authOptions) so session.user.id is defined
before using it in the Prisma query and downstream logic in BrandingPage and
BrandingForm.
- Around line 10-12: The page currently only checks for session?.user?.email and
allows any authenticated user; update the guard to enforce role-based
authorization by verifying session exists and that session.user.role is one of
"TENANT_ADMIN" or "SUPER_ADMIN" (mirroring the customer detail check) and call
redirect("/auth/login") when the check fails; locate the existing session usage
and redirect call in page.tsx and replace the simple email check with the
role-inclusive condition using session.user.role.

In @nextjs_space/app/tenant-admin/templates/page.tsx:
- Around line 63-65: Rename the misspelled variable templatess to a clearer name
like baseTemplates (or marketplaceTemplates) in the template listing statement
and update all its usages (e.g., where templatess is referenced for rendering or
logic) to the new identifier so references at other parts of the file compile
and reflect the new name.

In @nextjs_space/app/tenant-admin/the-wire/page.tsx:
- Around line 31-35: The query uses prisma.posts.findMany including the relation
as users (include: { users: true }) but the consumer component PostsList is
accessing post.author which doesn't exist; update the PostsList rendering to use
post.users (e.g., post.users?.name) wherever post.author is referenced, or
alternatively rename the relation consistently (either change include to author
or rename schema relation) so the relation symbol matches between
prisma.posts.findMany and PostsList.

In @nextjs_space/app/tenant-admin/webhooks/page.tsx:
- Around line 386-393: The webhook secret is shown in plain text (see the Label
and webhook.secret usage); add state to track visibility (e.g., visibleSecrets:
Set<string>) and a toggle function (e.g., toggleSecretVisibility(webhookId)) and
render the secret masked by default (e.g., replace characters with • or use
asterisks) unless the webhook's id is in visibleSecrets; add a reveal/hide
button or icon next to the masked value that calls toggleSecretVisibility and
include an accessible label (aria-pressed/aria-label) so only when toggled the
real webhook.secret is rendered.
- Around line 60-63: The two unused state vars
isEditDialogOpen/setIsEditDialogOpen and editingWebhook/setEditingWebhook are
declared but never referenced; either remove these state declarations from the
component or implement the edit flow: add an "Edit" button that calls
setEditingWebhook(webhook) and setIsEditDialogOpen(true), render an
EditWebhookDialog that reads editingWebhook and calls a save handler to update
the webhook list, and close the dialog by calling setIsEditDialogOpen(false) and
clearing setEditingWebhook(null). Ensure all references (isEditDialogOpen,
setIsEditDialogOpen, editingWebhook, setEditingWebhook) are either removed or
properly wired into the UI and handlers.
🧹 Nitpick comments (41)
nextjs_space/app/tenant-admin/seo/page.tsx (1)

101-106: Type assertion on pageSeo could be fragile.

The type assertion assumes the JSON structure in the database matches the expected shape. Consider adding runtime validation or using a schema validation library (e.g., Zod) to ensure data integrity, especially since this data comes from a JSON field that could have arbitrary content.

nextjs_space/app/tenant-admin/seo/seo-page-client.tsx (3)

61-67: Unused tenantId prop.

The tenantId prop is destructured but never used within the component. Either remove it from the props interface and destructuring, or verify if it's needed for future functionality.

#!/bin/bash
# Verify if tenantId is used anywhere in this file
rg -n 'tenantId' nextjs_space/app/tenant-admin/seo/seo-page-client.tsx

87-105: Consider adding error context to thrown errors.

The handler throws a generic error on failure. Including response details (status code, error message from body) would help with debugging and provide better error messages to users via the modal's catch block.

♻️ Suggested improvement
   const handleSaveProductSeo = async (seo: SeoData) => {
     if (!selectedProduct) return;

     const res = await fetch(
       `/api/tenant-admin/seo/products/${selectedProduct.id}`,
       {
         method: "PUT",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify(seo),
       },
     );

-    if (!res.ok) throw new Error("Failed to save");
+    if (!res.ok) {
+      const error = await res.json().catch(() => ({}));
+      throw new Error(error.message || `Failed to save (${res.status})`);
+    }

     // Update local state
     setLocalProducts((prev) =>
       prev.map((p) => (p.id === selectedProduct.id ? { ...p, seo } : p)),
     );
   };

142-156: Consider extracting SeoStatusBadge outside the component.

Defining this component inside SeoPageClient causes it to be recreated on every render. While the performance impact is minimal for this UI, extracting it outside the parent component (or memoizing it) is a cleaner pattern.

♻️ Suggested extraction
+const SeoStatusBadge = ({ hasCustomSeo }: { hasCustomSeo: boolean }) => (
+  <Badge variant={hasCustomSeo ? "default" : "secondary"} className="gap-1">
+    {hasCustomSeo ? (
+      <>
+        <CheckCircle className="h-3 w-3" />
+        Custom
+      </>
+    ) : (
+      <>
+        <AlertCircle className="h-3 w-3" />
+        Default
+      </>
+    )}
+  </Badge>
+);

 export function SeoPageClient({
   tenantId,
   baseUrl,
   products,
   posts,
   pageSeo,
 }: SeoPageClientProps) {
   // ... state hooks ...

-  const SeoStatusBadge = ({ hasCustomSeo }: { hasCustomSeo: boolean }) => (
-    <Badge variant={hasCustomSeo ? "default" : "secondary"} className="gap-1">
-      {hasCustomSeo ? (
-        <>
-          <CheckCircle className="h-3 w-3" />
-          Custom
-        </>
-      ) : (
-        <>
-          <AlertCircle className="h-3 w-3" />
-          Default
-        </>
-      )}
-    </Badge>
-  );
nextjs_space/app/tenant-admin/orders/[id]/packing-slip/page.tsx (1)

62-92: Consider adding AbortController for fetch cleanup.

The formatting changes look good. As a recommended improvement, consider adding an AbortController to prevent potential state updates on unmounted components when navigating away during fetch:

♻️ Suggested improvement with AbortController
 useEffect(() => {
   if (!session?.user?.id) return;

+  const abortController = new AbortController();
+
   const fetchOrderAndTenant = async () => {
     try {
       // Fetch order data
       const orderResponse = await fetch(
         `/api/tenant-admin/orders/${orderId}`,
+        { signal: abortController.signal },
       );
       if (!orderResponse.ok) {
         throw new Error("Failed to fetch order");
       }
       const orderData = await orderResponse.json();
       setOrder(orderData);

       // Fetch tenant data for business name
-      const tenantResponse = await fetch("/api/tenant-admin/tenant");
+      const tenantResponse = await fetch("/api/tenant-admin/tenant", {
+        signal: abortController.signal,
+      });
       if (tenantResponse.ok) {
         const tenantData = await tenantResponse.json();
         setTenant(tenantData);
       }
     } catch (error) {
+      if (error instanceof Error && error.name === "AbortError") return;
       console.error("Error fetching data:", error);
       toast.error("Failed to load packing slip");
     } finally {
       setLoading(false);
     }
   };

   fetchOrderAndTenant();
+
+  return () => abortController.abort();
 }, [session, orderId]);
nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx (1)

32-40: Consider removing unused include.

The tenant access verification logic is correct. However, the include: { tenants: true } fetches related data that's never used—only user?.tenantId is needed for the comparison.

♻️ Optional optimization
 const user = await prisma.users.findUnique({
   where: { id: session.user.id },
-  include: { tenants: true },
 });
nextjs_space/app/tenant-admin/audit-logs/page.tsx (1)

64-66: Consider adding fetchLogs to the dependency array or wrapping it with useCallback.

The useEffect calls fetchLogs but doesn't include it in the dependency array. While this works because the relevant state variables are listed, React's exhaustive-deps lint rule would flag this. Wrapping fetchLogs in useCallback with its dependencies would make the relationship explicit and satisfy linting.

♻️ Suggested refactor
+  const fetchLogs = useCallback(async () => {
-  const fetchLogs = async () => {
     try {
       setLoading(true);
       const params = new URLSearchParams({
         page: pagination.page.toString(),
         limit: pagination.limit.toString(),
       });

       if (actionFilter) params.append("action", actionFilter);
       if (entityFilter) params.append("entityType", entityFilter);

       const response = await fetch(`/api/tenant-admin/audit-logs?${params}`);
       const data = await response.json();

       if (response.ok) {
         setLogs(data.logs);
         setPagination(data.pagination);
       }
     } catch (error) {
       console.error("Failed to fetch audit logs:", error);
     } finally {
       setLoading(false);
     }
-  };
+  }, [pagination.page, pagination.limit, actionFilter, entityFilter]);

   useEffect(() => {
     fetchLogs();
-  }, [pagination.page, actionFilter, entityFilter]);
+  }, [fetchLogs]);

Don't forget to add useCallback to the imports:

-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } from "react";
nextjs_space/app/tenant-admin/the-wire/post-form.tsx (4)

17-17: Unused import: ArrowLeft

The ArrowLeft icon is imported but not used anywhere in the component.

🔧 Suggested fix
-import { ArrowLeft, Upload, X, Loader2 } from "lucide-react";
+import { Upload, X, Loader2 } from "lucide-react";

42-68: Consider improving error handling and code organization.

Two observations:

  1. Code organization: This function references form (line 59) before it's declared (line 70). While this works at runtime because the function is only called after form is initialized, moving the useForm hook before this function would improve readability.

  2. Error handling: The upload error handling discards any detailed error message from the server response.

🔧 Suggested improvement for error handling
      if (!res.ok) throw new Error("Upload failed");
+     if (!res.ok) {
+       const errorData = await res.json().catch(() => ({}));
+       throw new Error(errorData.error || "Upload failed");
+     }

183-203: Add accessibility attributes to the upload button.

The upload button with hidden file input overlay lacks an accessible label. Screen reader users won't understand the button's purpose.

♿ Suggested accessibility improvement
                        <Button
                          type="button"
                          variant="outline"
                          size="icon"
                          disabled={isUploading}
+                         aria-label="Upload cover image"
                        >

155-209: Consider extracting coverImage to a variable to avoid repeated watch calls.

form.watch("coverImage") is called multiple times (lines 158, 161). While functionally correct, extracting it to a variable improves readability and avoids redundant subscriptions.

🔧 Suggested improvement
+                const coverImage = form.watch("coverImage");
                 <div className="space-y-2">
                   <Label htmlFor="coverImage">Cover Image</Label>
                   <div className="flex flex-col gap-4">
-                    {form.watch("coverImage") && (
+                    {coverImage && (
                       <div className="relative aspect-video w-full max-w-sm rounded-lg overflow-hidden border">
                         <img
-                          src={form.watch("coverImage")}
+                          src={coverImage}
                           alt="Cover preview"
nextjs_space/app/tenant-admin/products/page.tsx (1)

98-102: Consider extracting the duplicated search filter.

The search filter OR clause is repeated four times across different Prisma queries. Extracting it to a reusable helper would improve maintainability.

♻️ Suggested refactor to eliminate duplication

Add this helper before the Prisma queries:

// Helper to build search filter for reuse
const searchFilter = search
  ? {
      OR: [
        { name: { contains: search, mode: "insensitive" } },
        { category: { contains: search, mode: "insensitive" } },
        { slug: { contains: search, mode: "insensitive" } },
      ],
    }
  : {};

Then simplify the queries:

 const whereClause: Prisma.productsWhereInput = {
   tenantId,
+  ...searchFilter,
 };

-// Apply search filter (case-insensitive across multiple fields)
-if (search) {
-  whereClause.OR = [
-    { name: { contains: search, mode: "insensitive" } },
-    { category: { contains: search, mode: "insensitive" } },
-    { slug: { contains: search, mode: "insensitive" } },
-  ];
-}

And in the parallel queries:

   prisma.products.count({
     where: {
       tenantId,
-      ...(search
-        ? {
-            OR: [
-              { name: { contains: search, mode: "insensitive" } },
-              { category: { contains: search, mode: "insensitive" } },
-              { slug: { contains: search, mode: "insensitive" } },
-            ],
-          }
-        : {}),
+      ...searchFilter,
       stock: { gt: 0 },
     },
   }),

Apply the same pattern to the other count queries.

Also applies to: 145-152, 161-168, 178-185

nextjs_space/app/tenant-admin/analytics/page.tsx (4)

248-255: Edge case: getInitials crashes on empty or single-character names.

If name is an empty string, n[0] returns undefined, and calling .join("") on an array containing undefined would produce unexpected results. Similarly, names without spaces return a single character.

Consider adding a guard:

Suggested improvement
 const getInitials = (name: string) => {
+  if (!name?.trim()) return "?";
   return name
     .split(" ")
-    .map((n) => n[0])
+    .map((n) => n[0] || "")
     .join("")
     .toUpperCase()
     .slice(0, 2);
 };

279-283: Missing session.user.id in dependency array may cause stale closure.

The effect checks session?.user?.id but only lists session in dependencies. While React's exhaustive-deps rule typically catches this, explicitly including session?.user?.id or restructuring makes the intent clearer.


303-390: Duplicate mock data objects violate DRY principle.

The same mock analytics object is defined twice—once in the else branch (lines 303-353) and again in the catch block (lines 358-390). Extract to a constant or helper function.

Suggested refactor
+const DEFAULT_MOCK_ANALYTICS: AnalyticsData = {
+  totalProducts: 10,
+  totalOrders: 25,
+  totalCustomers: 15,
+  totalRevenue: 2500.0,
+  recentOrders: 8,
+  recentCustomers: 5,
+  recentRevenue: 850.0,
+  avgOrderValue: 100.0,
+  revenueByDay: Array.from({ length: 7 }, (_, i) => ({
+    date: `Day ${i + 1}`,
+    revenue: Math.random() * 500 + 200,
+  })),
+  // ... rest of properties
+};

 const fetchAnalytics = async () => {
   try {
     // ...
     } else {
       console.error("API error:", response.status, response.statusText);
-      setAnalytics({
-        totalProducts: 10,
-        // ... duplicate object
-      });
+      setAnalytics(DEFAULT_MOCK_ANALYTICS);
     }
   } catch (error) {
     console.error("Error fetching analytics:", error);
-    setAnalytics({
-      totalProducts: 10,
-      // ... duplicate object
-    });
+    setAnalytics(DEFAULT_MOCK_ANALYTICS);
   }
 };

396-396: Consider moving COLORS constant outside the component.

The COLORS array is static and doesn't depend on component state or props. Defining it outside the component avoids recreating the array on every render.

Suggested fix
+const COLORS = ["#10b981", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6"];
+
 export default function TenantAnalyticsPage() {
   // ...
-  const COLORS = ["#10b981", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6"];
nextjs_space/app/tenant-admin/emails/[id]/client.tsx (1)

28-51: Consider extracting detailed error messages from the API response.

The error handling currently throws a generic error message when the response is not ok. Consider extracting the error details from the response body to provide more helpful feedback to users.

♻️ Suggested improvement for error handling
 const handleSave = async (data: EmailTemplateData) => {
   setIsSaving(true);
   try {
     const res = await fetch(
       `/api/tenant-admin/email-templates/${template.id}`,
       {
         method: "PUT",
         headers: { "Content-Type": "application/json" },
         body: JSON.stringify(data),
       },
     );

-    if (!res.ok) throw new Error("Failed to update template");
+    if (!res.ok) {
+      const errorData = await res.json().catch(() => ({}));
+      throw new Error(errorData.message || "Failed to update template");
+    }

     toast.success("Template updated successfully");
     router.refresh();
     router.push("/tenant-admin/emails");
   } catch (error) {
     console.error(error);
-    toast.error("Failed to update template");
+    toast.error(error instanceof Error ? error.message : "Failed to update template");
   } finally {
     setIsSaving(false);
   }
 };
nextjs_space/app/tenant-admin/emails/[id]/page.tsx (1)

44-44: Consider adding proper type definition instead of as any.

The as any type assertion bypasses TypeScript's type safety. Consider defining a proper type for the template props or using a type that matches the Prisma model.

♻️ Suggested improvement for type safety

If the EditTemplateClientProps interface in client.tsx matches the Prisma model structure, you could cast to that specific type:

-return <TenantEditTemplateClient template={template as any} />;
+return <TenantEditTemplateClient template={template} />;

Or define a serialization helper if Prisma types (Decimal, Date) need conversion:

function serializeTemplate(template: typeof template) {
  return {
    id: template.id,
    name: template.name,
    subject: template.subject,
    contentHtml: template.contentHtml,
    description: template.description,
    category: template.category,
  };
}

return <TenantEditTemplateClient template={serializeTemplate(template)} />;
nextjs_space/app/tenant-admin/emails/page.tsx (1)

32-33: Remove development artifacts.

The whitespace pattern {" "} and developer comment appear to be leftover from development and should be cleaned up before merging.

♻️ Proposed cleanup
-        <div className="flex justify-start">
-          {" "}
-          {/* Left aligned tabs usually better? Or right? User screenshot had Filter/Tabs. I'll stick to left or standard. */}
-          <TabsList>
+        <TabsList>
-            <TabsTrigger value="templates">Templates</TabsTrigger>
-            <TabsTrigger value="events">Event Triggers</TabsTrigger>
-          </TabsList>
-
-        </div>
+          <TabsTrigger value="templates">Templates</TabsTrigger>
+          <TabsTrigger value="events">Event Triggers</TabsTrigger>
+        </TabsList>
nextjs_space/app/tenant-admin/webhooks/page.tsx (2)

94-97: Consider adding URL format validation.

The current validation only checks if the URL field is not empty, but doesn't verify that it's a valid HTTP/HTTPS URL. Invalid URLs could cause runtime errors when webhooks are triggered.

✨ Proposed enhancement: Add URL validation
  const handleCreate = async () => {
-   if (!formData.url || formData.events.length === 0) {
-     toast.error("Please fill in all required fields");
+   if (!formData.url || formData.events.length === 0) {
+     toast.error("Please fill in all required fields");
+     return;
+   }
+
+   try {
+     new URL(formData.url);
+     if (!formData.url.startsWith('http://') && !formData.url.startsWith('https://')) {
+       toast.error("Webhook URL must use HTTP or HTTPS protocol");
+       return;
+     }
+   } catch {
+     toast.error("Please enter a valid URL");
      return;
    }

147-150: Replace confirm() with a Dialog component for consistency.

The delete confirmation uses the browser's native confirm() dialog, which is inconsistent with the Dialog components used elsewhere in this page (e.g., create webhook dialog). This creates an inconsistent user experience.

♻️ Proposed refactor: Use Dialog component

Add state for delete confirmation dialog:

const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [deletingWebhookId, setDeletingWebhookId] = useState<string | null>(null);

Update the handler:

- const handleDelete = async (webhookId: string) => {
-   if (!confirm("Are you sure you want to delete this webhook?")) {
-     return;
-   }
+ const confirmDelete = (webhookId: string) => {
+   setDeletingWebhookId(webhookId);
+   setDeleteConfirmOpen(true);
+ };

+ const handleDelete = async () => {
+   if (!deletingWebhookId) return;
+
    try {
-     const response = await fetch(`/api/tenant-admin/webhooks/${webhookId}`, {
+     const response = await fetch(`/api/tenant-admin/webhooks/${deletingWebhookId}`, {
        method: "DELETE",
      });
      
      if (response.ok) {
        toast.success("Webhook deleted successfully");
+       setDeleteConfirmOpen(false);
+       setDeletingWebhookId(null);
        fetchWebhooks();

Then add the Dialog component in the JSX.

nextjs_space/app/tenant-admin/cookie-settings/settings-form.tsx (1)

36-40: Unused tenantId prop.

The tenantId prop is declared in the interface and destructured but never used in the component. If the API endpoint derives the tenant from the session, this prop can be removed to avoid confusion.

🔧 Suggested fix
 interface CookieSettingsFormProps {
-  tenantId: string;
   countryCode: string;
   initialSettings: {
     // ...
   };
 }

 export default function CookieSettingsForm({
-  tenantId,
   countryCode,
   initialSettings,
 }: CookieSettingsFormProps) {
nextjs_space/app/tenant-admin/orders/orders-table.tsx (2)

1015-1040: Consider using Loader2 consistently instead of inline SVG.

The Loader2 icon from lucide-react is already imported and used elsewhere in this file (lines 71, 723). Using it here instead of the inline SVG would improve consistency and reduce code duplication.

🔧 Suggested fix
             {isProcessing ? (
                <>
-                 <span className="animate-spin mr-2">
-                   <svg
-                     className="h-4 w-4"
-                     viewBox="0 0 24 24"
-                     fill="none"
-                     xmlns="http://www.w3.org/2000/svg"
-                   >
-                     <circle
-                       className="opacity-25"
-                       cx="12"
-                       cy="12"
-                       r="10"
-                       stroke="currentColor"
-                       strokeWidth="4"
-                     />
-                     <path
-                       className="opacity-75"
-                       fill="currentColor"
-                       d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
-                     />
-                   </svg>
-                 </span>
+                 <Loader2 className="mr-2 h-4 w-4 animate-spin" />
                  Processing...
                </>

294-309: Minor: getDateRangeLabel function called in useMemo without being in dependencies.

The getDateRangeLabel function is called within the useMemo but not listed in the dependency array. While the underlying state values (dateRangeFilter, dateFromFilter, dateToFilter) influence the result indirectly through hasDateFilter, this pattern may trigger ESLint's exhaustive-deps rule. Consider memoizing getDateRangeLabel with useCallback or including the actual state values in the dependency array.

nextjs_space/app/tenant-admin/customers/[id]/customer-edit-form.tsx (1)

49-51: Consider using unknown type instead of any for caught errors.

TypeScript best practice is to use unknown for caught errors and then narrow the type, rather than using any.

🔧 Suggested fix
-    } catch (error: any) {
-      toast.error(error.message || "Failed to update customer");
+    } catch (error: unknown) {
+      const message = error instanceof Error ? error.message : "Failed to update customer";
+      toast.error(message);
       console.error(error);
nextjs_space/app/tenant-admin/templates/clone-button.tsx (2)

45-47: Inconsistent error handling: alert() vs toast.

This component uses alert() for error notifications while the rest of the codebase (including activate-button.tsx in the same directory) uses the toast utility. Consider using toast.error() for consistency.

🔧 Suggested fix
+import { toast } from "@/components/ui/sonner";
 // ... in catch block:
     } catch (error) {
       console.error("Clone error:", error);
-      alert("Failed to clone template. Please try again.");
+      toast.error("Failed to clone template. Please try again.");
     } finally {

8-16: Unused templateName prop.

The templateName prop is declared in the interface and destructured but never used in the component. If it's intended for future use (e.g., in success/error messages), consider using it; otherwise, remove it.

🔧 Suggested fix (if not needed)
 interface TemplateCloneButtonProps {
   templateId: string;
-  templateName: string;
 }

 export default function TemplateCloneButton({
   templateId,
-  templateName,
 }: TemplateCloneButtonProps) {
nextjs_space/app/tenant-admin/templates/page.tsx (1)

24-24: Avoid using any type.

The ClonedTemplate type is set to any, which bypasses TypeScript's type checking benefits. Consider defining a proper type based on the Prisma query result.

🔧 Suggested fix
-// Define typed interface for the cloned template with the included base template relation
-type ClonedTemplate = any;
+// Define typed interface for the cloned template with the included base template relation
+type ClonedTemplate = {
+  id: string;
+  tenantId: string;
+  templatesId: string;
+  templateName: string;
+  isActive: boolean;
+  createdAt: Date;
+  templates: {
+    thumbnailUrl: string | null;
+    previewUrl: string | null;
+  } | null;
+};
nextjs_space/app/auth/forgot-password/page.tsx (1)

37-41: Consider using toast notifications instead of alert() for consistency.

Other files in this PR (e.g., orders/page.tsx, posts-list.tsx, customer-actions.tsx) use the toast utility from @/components/ui/sonner for user feedback. Using alert() here is inconsistent and provides a worse UX.

♻️ Suggested improvement
+import { toast } from "@/components/ui/sonner";
 
 // In handleSubmit:
       } else {
-        alert(data.error || "Failed to send reset email");
+        toast.error(data.error || "Failed to send reset email");
       }
     } catch (error) {
       console.error("Password reset error:", error);
-      alert("Failed to send reset email");
+      toast.error("Failed to send reset email");
     } finally {
nextjs_space/app/tenant-admin/customers/customers-table.tsx (1)

193-200: Consider adding clipboard API availability check.

navigator.clipboard.writeText may not be available in all contexts (e.g., non-HTTPS in some browsers). A try-catch with user feedback would improve robustness.

♻️ Optional improvement
              onClick: () => {
-               navigator.clipboard.writeText(window.location.origin);
+               navigator.clipboard.writeText(window.location.origin)
+                 .then(() => toast.success("Store URL copied!"))
+                 .catch(() => toast.error("Failed to copy URL"));
              },
nextjs_space/app/tenant-admin/the-wire/posts-list.tsx (2)

29-31: Consider typing initialPosts with the Post interface.

The Post interface is defined but initialPosts is typed as any[], losing type safety benefits.

♻️ Suggested fix
-export default function PostsList({ initialPosts }: { initialPosts: any[] }) {
+export default function PostsList({ initialPosts }: { initialPosts: Post[] }) {
   const router = useRouter();
-  const [posts, setPosts] = useState(initialPosts);
+  const [posts, setPosts] = useState<Post[]>(initialPosts);

58-59: Consider using AlertDialog instead of window.confirm() for consistency.

Other components in this PR (e.g., customer-actions.tsx) use AlertDialog for delete confirmations, providing better UX and consistent styling.

nextjs_space/app/tenant-admin/orders/page.tsx (1)

109-119: Client-side auth check may cause brief content flash.

Unlike the server components in this PR (e.g., customers/page.tsx, settings/page.tsx) that redirect before rendering, this client-side check allows the page to briefly render before redirecting unauthorized users. Consider adding server-side protection via middleware or a server component wrapper.

nextjs_space/app/auth/login/page.tsx (1)

41-80: Consider adding cleanup for fetch requests.

The useEffect makes fetch calls that could complete after component unmount, potentially causing state updates on unmounted components. While React 18+ handles this more gracefully, adding an AbortController would be a cleaner pattern.

♻️ Suggested improvement with AbortController
 useEffect(() => {
+  const controller = new AbortController();
   // Get tenant from hostname/subdomain
   const hostname = window.location.hostname;
   const parts = hostname.split(".");
   const subdomain = parts.length >= 2 ? parts[0] : null;

   // Fetch tenant settings based on subdomain
   if (subdomain && subdomain !== "localhost" && subdomain !== "www") {
-    fetch(`/api/tenant/${subdomain}`)
+    fetch(`/api/tenant/${subdomain}`, { signal: controller.signal })
       .then((res) => res.json())
       .then((data) => {
         if (data && data.tenant) {
           setTenantSettings({
             businessName: data.tenant.businessName || "BudStack",
             primaryColor: data.tenant.branding?.primaryColor || "#16a34a",
             logoUrl: data.tenant.branding?.logoUrl,
             subdomain: data.tenant.subdomain,
           });
         }
       })
       .catch((err) => {
-        console.error("Failed to fetch tenant settings:", err);
+        if (err.name !== 'AbortError') {
+          console.error("Failed to fetch tenant settings:", err);
+        }
       });
   } else {
     // ... similar changes for platform settings fetch
   }
+  return () => controller.abort();
 }, []);
nextjs_space/app/tenant-admin/branding/comprehensive-branding-form.tsx (1)

301-312: Type assertions for dynamic keys could be made safer.

The dynamic key access pattern using formData[\${day}Open` as keyof typeof formData]` works but relies on type assertions. This is acceptable given the initialization guarantees these keys exist, but consider extracting to a helper function for better maintainability in the future.

nextjs_space/app/tenant-admin/page.tsx (1)

66-67: Remove redundant variable declaration.

Both tenantStoreUrl and tenantUrl are assigned the same value from getTenantUrl(tenant). Only one variable is needed.

♻️ Suggested fix
-  const tenantStoreUrl = getTenantUrl(tenant);
-  const tenantUrl = getTenantUrl(tenant);
+  const tenantUrl = getTenantUrl(tenant);

Then replace tenantStoreUrl with tenantUrl on lines 86 and 89.

nextjs_space/app/tenant-admin/customers/[id]/page.tsx (2)

17-22: Non-null assertion on session.user.role could be avoided.

Using session.user.role! suppresses TypeScript's null check. While the logic still works correctly (an undefined role won't match the array and will redirect), removing the assertion makes the code more explicit.

♻️ Suggested improvement
   if (
     !session ||
-    !["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role!)
+    !session.user.role ||
+    !["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role)
   ) {
     redirect("/auth/login");
   }

48-51: Redundant tenant access check - already enforced by the Prisma query.

This verification is unnecessary because the Prisma query on lines 28-42 already includes ...(tenantId && { tenantId }). When tenantId is defined (for TENANT_ADMIN), only customers matching that tenant are returned. If a customer is found, it's guaranteed to have the correct tenantId.

This doesn't cause harm but can be removed to reduce code noise.

♻️ Remove redundant check
   if (!customer) {
     notFound();
   }

-  // Verify tenant access for tenant admins
-  if (session.user.role === "TENANT_ADMIN" && customer.tenantId !== tenantId) {
-    notFound();
-  }
-
   return (
nextjs_space/app/onboarding/onboarding-form.tsx (3)

114-119: Consider allowing hyphens in generated subdomains.

The regex /[^a-z0-9]/g strips hyphens, which are valid subdomain characters. For business names like "Green-Leaf Dispensary", preserving word boundaries with hyphens (e.g., greenleafgreen-leaf) would improve readability.

That said, users can manually edit the subdomain field, so this is a minor UX consideration.


121-134: Consider adding basic format validation.

The validation checks for presence but not format:

  • Email: No format validation (could use a simple regex or native HTML5 validation)
  • Subdomain: User-edited value isn't validated for valid subdomain characters

While server-side validation should catch these, client-side feedback improves UX.

♻️ Suggested validation improvements
 const validateStep = (step: number) => {
+  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+  const subdomainRegex = /^[a-z0-9][a-z0-9-]{0,61}[a-z0-9]?$/;
+
   switch (step) {
     case 1:
-      return formData.businessName && formData.subdomain;
+      return formData.businessName && formData.subdomain && subdomainRegex.test(formData.subdomain);
     case 2:
-      return formData.email && formData.password.length >= 6;
+      return emailRegex.test(formData.email) && formData.password.length >= 6;
     case 3:
       return formData.nftTokenId && formData.countryCode;
     case 4:
       return formData.templateId;
     default:
       return false;
   }
 };

433-438: Consider using Next.js Image component for optimization.

Native <img> tags miss Next.js image optimization benefits (lazy loading, responsive sizing, format optimization). If template images are from known domains, configure next.config.js and use the Image component.

This is optional since external URLs require explicit domain configuration.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 08176f9 and af12a67.

📒 Files selected for processing (43)
  • nextjs_space/app/auth/forgot-password/page.tsx
  • nextjs_space/app/auth/login/page.tsx
  • nextjs_space/app/auth/reset-password/[token]/page.tsx
  • nextjs_space/app/auth/signup/page.tsx
  • nextjs_space/app/onboarding/onboarding-form.tsx
  • nextjs_space/app/onboarding/page.tsx
  • nextjs_space/app/tenant-admin/analytics/page.tsx
  • nextjs_space/app/tenant-admin/audit-logs/page.tsx
  • nextjs_space/app/tenant-admin/branding/branding-form.tsx
  • nextjs_space/app/tenant-admin/branding/comprehensive-branding-form.tsx
  • nextjs_space/app/tenant-admin/branding/page.tsx
  • nextjs_space/app/tenant-admin/cookie-settings/page.tsx
  • nextjs_space/app/tenant-admin/cookie-settings/settings-form.tsx
  • nextjs_space/app/tenant-admin/customers/[id]/customer-actions.tsx
  • nextjs_space/app/tenant-admin/customers/[id]/customer-edit-form.tsx
  • nextjs_space/app/tenant-admin/customers/[id]/page.tsx
  • nextjs_space/app/tenant-admin/customers/customers-table.tsx
  • nextjs_space/app/tenant-admin/customers/page.tsx
  • nextjs_space/app/tenant-admin/emails/[id]/client.tsx
  • nextjs_space/app/tenant-admin/emails/[id]/page.tsx
  • nextjs_space/app/tenant-admin/emails/new/client.tsx
  • nextjs_space/app/tenant-admin/emails/new/page.tsx
  • nextjs_space/app/tenant-admin/emails/page.tsx
  • nextjs_space/app/tenant-admin/layout.tsx
  • nextjs_space/app/tenant-admin/orders/[id]/packing-slip/page.tsx
  • nextjs_space/app/tenant-admin/orders/orders-table.tsx
  • nextjs_space/app/tenant-admin/orders/page.tsx
  • nextjs_space/app/tenant-admin/page.tsx
  • nextjs_space/app/tenant-admin/products/page.tsx
  • nextjs_space/app/tenant-admin/products/products-table.tsx
  • nextjs_space/app/tenant-admin/seo/page.tsx
  • nextjs_space/app/tenant-admin/seo/seo-page-client.tsx
  • nextjs_space/app/tenant-admin/settings/page.tsx
  • nextjs_space/app/tenant-admin/settings/settings-form.tsx
  • nextjs_space/app/tenant-admin/templates/activate-button.tsx
  • nextjs_space/app/tenant-admin/templates/clone-button.tsx
  • nextjs_space/app/tenant-admin/templates/page.tsx
  • nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx
  • nextjs_space/app/tenant-admin/the-wire/new/page.tsx
  • nextjs_space/app/tenant-admin/the-wire/page.tsx
  • nextjs_space/app/tenant-admin/the-wire/post-form.tsx
  • nextjs_space/app/tenant-admin/the-wire/posts-list.tsx
  • nextjs_space/app/tenant-admin/webhooks/page.tsx
🧰 Additional context used
🧬 Code graph analysis (33)
nextjs_space/app/tenant-admin/emails/[id]/client.tsx (2)
nextjs_space/components/admin/email/EmailEditor.tsx (1)
  • EmailTemplateData (23-29)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/app/tenant-admin/settings/page.tsx (2)
nextjs_space/lib/auth.ts (2)
  • session (64-71)
  • authOptions (7-80)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/app/tenant-admin/orders/[id]/packing-slip/page.tsx (1)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/app/tenant-admin/emails/[id]/page.tsx (3)
nextjs_space/lib/auth.ts (2)
  • session (64-71)
  • authOptions (7-80)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/tenant-admin/emails/[id]/client.tsx (1)
  • TenantEditTemplateClient (22-70)
nextjs_space/app/tenant-admin/cookie-settings/page.tsx (3)
nextjs_space/lib/auth.ts (1)
  • session (64-71)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/tenant-admin/cookie-settings/settings-form.tsx (1)
  • CookieSettingsForm (36-264)
nextjs_space/app/tenant-admin/products/page.tsx (4)
nextjs_space/lib/auth.ts (1)
  • session (64-71)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/components/admin/shared/index.ts (1)
  • Breadcrumbs (28-28)
nextjs_space/app/tenant-admin/audit-logs/page.tsx (1)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/app/tenant-admin/cookie-settings/settings-form.tsx (7)
nextjs_space/lib/cookie-utils.ts (3)
  • getConsentModel (76-93)
  • isGDPRRegion (98-103)
  • isPOPIARegion (108-111)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/components/ui/card.tsx (5)
  • Card (80-80)
  • CardHeader (81-81)
  • CardTitle (83-83)
  • CardContent (85-85)
  • CardDescription (84-84)
nextjs_space/components/ui/label.tsx (1)
  • Label (26-26)
nextjs_space/components/ui/switch.tsx (1)
  • Switch (29-29)
nextjs_space/components/ui/input.tsx (1)
  • Input (24-24)
nextjs_space/components/ui/button.tsx (1)
  • Button (57-57)
nextjs_space/app/tenant-admin/the-wire/new/page.tsx (3)
nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx (1)
  • metadata (7-9)
nextjs_space/app/tenant-admin/the-wire/page.tsx (1)
  • metadata (11-13)
nextjs_space/app/tenant-admin/the-wire/post-form.tsx (1)
  • PostForm (34-274)
nextjs_space/app/tenant-admin/layout.tsx (1)
nextjs_space/lib/auth.ts (1)
  • session (64-71)
nextjs_space/app/tenant-admin/templates/clone-button.tsx (1)
nextjs_space/components/ui/button.tsx (1)
  • Button (57-57)
nextjs_space/app/tenant-admin/emails/new/client.tsx (2)
nextjs_space/components/admin/email/EmailEditor.tsx (1)
  • EmailTemplateData (23-29)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/app/tenant-admin/branding/comprehensive-branding-form.tsx (9)
nextjs_space/lib/types.ts (1)
  • TenantSettings (35-210)
nextjs_space/components/ui/tabs.tsx (1)
  • TabsTrigger (55-55)
nextjs_space/components/ui/label.tsx (1)
  • Label (26-26)
nextjs_space/components/ui/input.tsx (1)
  • Input (24-24)
nextjs_space/components/ui/separator.tsx (1)
  • Separator (31-31)
nextjs_space/components/ui/switch.tsx (1)
  • Switch (29-29)
nextjs_space/components/ui/select.tsx (1)
  • Select (150-150)
nextjs_space/components/ui/card.tsx (3)
  • CardDescription (84-84)
  • CardHeader (81-81)
  • CardContent (85-85)
nextjs_space/components/ui/textarea.tsx (1)
  • Textarea (23-23)
nextjs_space/app/tenant-admin/seo/page.tsx (3)
nextjs_space/lib/auth.ts (1)
  • session (64-71)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/tenant-admin/seo/seo-page-client.tsx (1)
  • SeoPageClient (61-381)
nextjs_space/app/tenant-admin/customers/page.tsx (2)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/app/tenant-admin/customers/customers-table.tsx (1)
  • CustomersTable (61-320)
nextjs_space/app/tenant-admin/orders/page.tsx (2)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/components/ui/dialog.tsx (2)
  • Dialog (112-112)
  • DialogContent (117-117)
nextjs_space/app/tenant-admin/products/products-table.tsx (4)
nextjs_space/lib/utils.ts (1)
  • cn (4-6)
nextjs_space/lib/admin/url-state.ts (1)
  • useTableState (136-322)
nextjs_space/components/admin/shared/index.ts (1)
  • StatusFilterOption (13-13)
nextjs_space/lib/admin/csv-export.ts (1)
  • exportToCSV (116-147)
nextjs_space/app/tenant-admin/customers/customers-table.tsx (2)
nextjs_space/lib/admin/url-state.ts (1)
  • useTableState (136-322)
nextjs_space/lib/admin/csv-export.ts (1)
  • exportToCSV (116-147)
nextjs_space/app/tenant-admin/customers/[id]/customer-actions.tsx (2)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/components/ui/alert-dialog.tsx (8)
  • AlertDialog (130-130)
  • AlertDialogContent (134-134)
  • AlertDialogHeader (135-135)
  • AlertDialogTitle (137-137)
  • AlertDialogDescription (138-138)
  • AlertDialogFooter (136-136)
  • AlertDialogCancel (140-140)
  • AlertDialogAction (139-139)
nextjs_space/app/tenant-admin/emails/new/page.tsx (2)
nextjs_space/lib/auth.ts (2)
  • session (64-71)
  • authOptions (7-80)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/tenant-admin/settings/settings-form.tsx (1)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/app/auth/login/page.tsx (3)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/lib/auth.ts (1)
  • session (64-71)
nextjs_space/components/ui/button.tsx (1)
  • Button (57-57)
nextjs_space/app/tenant-admin/the-wire/posts-list.tsx (4)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/components/ui/button.tsx (1)
  • Button (57-57)
nextjs_space/components/ui/table.tsx (6)
  • Table (109-109)
  • TableHeader (110-110)
  • TableRow (114-114)
  • TableHead (113-113)
  • TableBody (111-111)
  • TableCell (115-115)
nextjs_space/components/ui/badge.tsx (1)
  • Badge (37-37)
nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx (2)
nextjs_space/lib/auth.ts (2)
  • session (64-71)
  • authOptions (7-80)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/tenant-admin/customers/[id]/customer-edit-form.tsx (5)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/components/ui/card.tsx (4)
  • Card (80-80)
  • CardHeader (81-81)
  • CardTitle (83-83)
  • CardContent (85-85)
nextjs_space/components/ui/button.tsx (1)
  • Button (57-57)
nextjs_space/components/ui/label.tsx (1)
  • Label (26-26)
nextjs_space/components/ui/input.tsx (1)
  • Input (24-24)
nextjs_space/app/onboarding/onboarding-form.tsx (7)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/components/ui/label.tsx (1)
  • Label (26-26)
nextjs_space/components/ui/input.tsx (1)
  • Input (24-24)
nextjs_space/components/ui/textarea.tsx (1)
  • Textarea (23-23)
nextjs_space/components/ui/badge.tsx (1)
  • Badge (37-37)
nextjs_space/components/ui/card.tsx (5)
  • Card (80-80)
  • CardHeader (81-81)
  • CardTitle (83-83)
  • CardDescription (84-84)
  • CardContent (85-85)
nextjs_space/components/ui/button.tsx (1)
  • Button (57-57)
nextjs_space/app/tenant-admin/templates/activate-button.tsx (1)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/app/tenant-admin/the-wire/page.tsx (3)
nextjs_space/app/tenant-admin/the-wire/new/page.tsx (1)
  • metadata (3-5)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/app/tenant-admin/the-wire/posts-list.tsx (1)
  • PostsList (29-178)
nextjs_space/app/tenant-admin/branding/page.tsx (2)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/components/admin/shared/index.ts (1)
  • Breadcrumbs (28-28)
nextjs_space/app/tenant-admin/the-wire/post-form.tsx (7)
nextjs_space/components/ui/sonner.tsx (1)
  • toast (26-170)
nextjs_space/components/ui/card.tsx (2)
  • Card (80-80)
  • CardContent (85-85)
nextjs_space/components/ui/label.tsx (1)
  • Label (26-26)
nextjs_space/components/ui/input.tsx (1)
  • Input (24-24)
nextjs_space/components/ui/textarea.tsx (1)
  • Textarea (23-23)
nextjs_space/components/ui/button.tsx (1)
  • Button (57-57)
nextjs_space/components/ui/switch.tsx (1)
  • Switch (29-29)
nextjs_space/app/tenant-admin/templates/page.tsx (10)
nextjs_space/lib/auth.ts (2)
  • session (64-71)
  • authOptions (7-80)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/components/admin/shared/index.ts (1)
  • Breadcrumbs (28-28)
nextjs_space/components/ui/tabs.tsx (4)
  • Tabs (55-55)
  • TabsList (55-55)
  • TabsTrigger (55-55)
  • TabsContent (55-55)
nextjs_space/components/ui/card.tsx (6)
  • Card (80-80)
  • CardHeader (81-81)
  • CardTitle (83-83)
  • CardDescription (84-84)
  • CardContent (85-85)
  • CardFooter (82-82)
nextjs_space/components/ui/badge.tsx (1)
  • Badge (37-37)
nextjs_space/components/ui/button.tsx (1)
  • Button (57-57)
nextjs_space/app/tenant-admin/templates/activate-button.tsx (1)
  • ActivateButton (15-79)
nextjs_space/app/tenant-admin/templates/clone-button.tsx (1)
  • TemplateCloneButton (13-78)
nextjs_space/app/tenant-admin/analytics/page.tsx (2)
nextjs_space/components/admin/shared/Breadcrumbs.tsx (1)
  • Breadcrumbs (66-139)
nextjs_space/lib/utils.ts (1)
  • cn (4-6)
nextjs_space/app/tenant-admin/seo/seo-page-client.tsx (4)
nextjs_space/components/ui/badge.tsx (1)
  • Badge (37-37)
nextjs_space/components/ui/tabs.tsx (4)
  • Tabs (55-55)
  • TabsList (55-55)
  • TabsTrigger (55-55)
  • TabsContent (55-55)
nextjs_space/components/ui/card.tsx (5)
  • Card (80-80)
  • CardHeader (81-81)
  • CardTitle (83-83)
  • CardDescription (84-84)
  • CardContent (85-85)
nextjs_space/components/admin/seo/SeoEditorModal.tsx (1)
  • SeoEditorModal (37-192)
🪛 Biome (2.1.2)
nextjs_space/app/onboarding/onboarding-form.tsx

[error] 410-414: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

Comment on lines +385 to +400
<option value="PT">Portugal 🇵🇹</option>
<option value="SA">South Africa 🇿🇦</option>
<option value="UK">United Kingdom 🇬🇧</option>
<option value="DE">Germany 🇩🇪</option>
<option value="ES">Spain 🇪🇸</option>
<option value="FR">France 🇫🇷</option>
<option value="IT">Italy 🇮🇹</option>
<option value="NL">Netherlands 🇳🇱</option>
<option value="BE">Belgium 🇧🇪</option>
<option value="AT">Austria 🇦🇹</option>
<option value="IE">Ireland 🇮🇪</option>
<option value="CH">Switzerland 🇨🇭</option>
<option value="US">United States 🇺🇸</option>
<option value="CA">Canada 🇨🇦</option>
<option value="AU">Australia 🇦🇺</option>
</select>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "countryCode" --type=ts --type=tsx -g '!node_modules' -C3

Repository: AutomatosAI/budstack-saas

Length of output: 96


🏁 Script executed:

rg -n "\"UK\"|\"GB\"" --type=ts --type=tsx -g '!node_modules'

Repository: AutomatosAI/budstack-saas

Length of output: 96


🏁 Script executed:

rg -n "countries|country.*code" --type=ts --type=tsx -g '!node_modules' | head -50

Repository: AutomatosAI/budstack-saas

Length of output: 96


🏁 Script executed:

rg "countryCode" -C3 | head -100

Repository: AutomatosAI/budstack-saas

Length of output: 8401


🏁 Script executed:

rg "\"UK\"|\"GB\"" -C2 | head -100

Repository: AutomatosAI/budstack-saas

Length of output: 4087


🏁 Script executed:

find . -name "onboarding-form.tsx" -type f

Repository: AutomatosAI/budstack-saas

Length of output: 118


🏁 Script executed:

rg "formData|countryCode" --type ts -C3 | head -100

Repository: AutomatosAI/budstack-saas

Length of output: 7363


Standardize United Kingdom country code to ISO 3166-1 alpha-2 format.

The form uses the non-standard code "UK" while every other country dropdown in the application uses the ISO standard code "GB" (see consultation-constants.ts, ClientOnboarding.tsx, and all consultation form steps). This inconsistency may cause issues with downstream code that expects standard codes. While the cookie compliance utilities explicitly accept both, the rest of the system standardizes on "GB".

Suggested fix
-                <option value="UK">United Kingdom 🇬🇧</option>
+                <option value="GB">United Kingdom 🇬🇧</option>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<option value="PT">Portugal 🇵🇹</option>
<option value="SA">South Africa 🇿🇦</option>
<option value="UK">United Kingdom 🇬🇧</option>
<option value="DE">Germany 🇩🇪</option>
<option value="ES">Spain 🇪🇸</option>
<option value="FR">France 🇫🇷</option>
<option value="IT">Italy 🇮🇹</option>
<option value="NL">Netherlands 🇳🇱</option>
<option value="BE">Belgium 🇧🇪</option>
<option value="AT">Austria 🇦🇹</option>
<option value="IE">Ireland 🇮🇪</option>
<option value="CH">Switzerland 🇨🇭</option>
<option value="US">United States 🇺🇸</option>
<option value="CA">Canada 🇨🇦</option>
<option value="AU">Australia 🇦🇺</option>
</select>
<option value="PT">Portugal 🇵🇹</option>
<option value="SA">South Africa 🇿🇦</option>
<option value="GB">United Kingdom 🇬🇧</option>
<option value="DE">Germany 🇩🇪</option>
<option value="ES">Spain 🇪🇸</option>
<option value="FR">France 🇫🇷</option>
<option value="IT">Italy 🇮🇹</option>
<option value="NL">Netherlands 🇳🇱</option>
<option value="BE">Belgium 🇧🇪</option>
<option value="AT">Austria 🇦🇹</option>
<option value="IE">Ireland 🇮🇪</option>
<option value="CH">Switzerland 🇨🇭</option>
<option value="US">United States 🇺🇸</option>
<option value="CA">Canada 🇨🇦</option>
<option value="AU">Australia 🇦🇺</option>
</select>
🤖 Prompt for AI Agents
In @nextjs_space/app/onboarding/onboarding-form.tsx around lines 385 - 400, The
United Kingdom option in the country <select> in onboarding-form.tsx uses the
non-standard value "UK"; change its value to the ISO 3166-1 alpha-2 code "GB"
(i.e., update the <option value="UK">… to <option value="GB">…). After changing,
search this component for any comparisons, default values, or form state that
reference "UK" and update them to "GB" to keep behavior consistent with
consultation-constants.ts and other components.

Comment on lines +408 to +485
case 4:
const selectedTemplate = templates.find(
(t) => t.id === formData.templateId,
);
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">
Choose Your Store Template
</h3>
<div className="grid grid-cols-2 gap-4">
{templates.map((template) => (
<button
key={template.id}
type="button"
onClick={() =>
setFormData({ ...formData, templateId: template.id })
}
className={`p-4 border-2 rounded-lg text-left transition-all hover:shadow-lg ${
formData.templateId === template.id
? "border-primary bg-primary/5"
: "border-gray-200 hover:border-gray-300"
}`}
>
<div className="flex items-center gap-3 mb-3">
{template.thumbnailUrl ? (
<img
src={template.thumbnailUrl}
alt={template.name}
className="w-12 h-12 object-cover rounded-lg"
/>
) : (
<div className="w-12 h-12 rounded-lg bg-slate-100 flex items-center justify-center">
<Palette className="h-6 w-6 text-slate-400" />
</div>

{selectedTemplate && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2">
<Eye className="h-4 w-4" />
Preview: {selectedTemplate.name}
</h4>
{selectedTemplate.previewUrl && (
<div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100">
<img src={selectedTemplate.previewUrl} alt="Preview" className="w-full h-full object-cover" />
</div>
)}
<p className="text-xs text-green-800 mt-3">
You can customize everything later from your admin dashboard.
</p>
</div>
)}
<div className="flex-1">
<div className="font-semibold">{template.name}</div>
{formData.templateId === template.id && (
<Badge className="mt-1">Selected</Badge>
)}
</div>
</div>
);
<p className="text-sm text-gray-600 line-clamp-2">
{template.description}
</p>
</button>
))}
</div>
</div>

default:
return null;
}
};
{selectedTemplate && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2">
<Eye className="h-4 w-4" />
Preview: {selectedTemplate.name}
</h4>
{selectedTemplate.previewUrl && (
<div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100">
<img
src={selectedTemplate.previewUrl}
alt="Preview"
className="w-full h-full object-cover"
/>
</div>
)}
<p className="text-xs text-green-800 mt-3">
You can customize everything later from your admin dashboard.
</p>
</div>
)}
</div>
);

return (
<div className="min-h-screen bg-gradient-to-b from-green-50 to-white py-12 px-4">
<div className="max-w-3xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">Join BudStack</h1>
<p className="text-lg text-gray-600">Launch your medical cannabis dispensary in minutes</p>
</div>
default:
return null;
}
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Wrap case 4 in a block to scope the variable declaration.

The selectedTemplate variable declared on line 409 is flagged by static analysis (Biome) because declarations in switch cases without block scope can be accessed from other cases. While this is unlikely to cause issues here since each case returns, wrapping in a block is the correct pattern.

♻️ Suggested fix
       case 4:
+      {
         const selectedTemplate = templates.find(
           (t) => t.id === formData.templateId,
         );
         return (
           <div className="space-y-6">
             // ... rest of the JSX
           </div>
         );
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case 4:
const selectedTemplate = templates.find(
(t) => t.id === formData.templateId,
);
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">
Choose Your Store Template
</h3>
<div className="grid grid-cols-2 gap-4">
{templates.map((template) => (
<button
key={template.id}
type="button"
onClick={() =>
setFormData({ ...formData, templateId: template.id })
}
className={`p-4 border-2 rounded-lg text-left transition-all hover:shadow-lg ${
formData.templateId === template.id
? "border-primary bg-primary/5"
: "border-gray-200 hover:border-gray-300"
}`}
>
<div className="flex items-center gap-3 mb-3">
{template.thumbnailUrl ? (
<img
src={template.thumbnailUrl}
alt={template.name}
className="w-12 h-12 object-cover rounded-lg"
/>
) : (
<div className="w-12 h-12 rounded-lg bg-slate-100 flex items-center justify-center">
<Palette className="h-6 w-6 text-slate-400" />
</div>
{selectedTemplate && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2">
<Eye className="h-4 w-4" />
Preview: {selectedTemplate.name}
</h4>
{selectedTemplate.previewUrl && (
<div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100">
<img src={selectedTemplate.previewUrl} alt="Preview" className="w-full h-full object-cover" />
</div>
)}
<p className="text-xs text-green-800 mt-3">
You can customize everything later from your admin dashboard.
</p>
</div>
)}
<div className="flex-1">
<div className="font-semibold">{template.name}</div>
{formData.templateId === template.id && (
<Badge className="mt-1">Selected</Badge>
)}
</div>
</div>
);
<p className="text-sm text-gray-600 line-clamp-2">
{template.description}
</p>
</button>
))}
</div>
</div>
default:
return null;
}
};
{selectedTemplate && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2">
<Eye className="h-4 w-4" />
Preview: {selectedTemplate.name}
</h4>
{selectedTemplate.previewUrl && (
<div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100">
<img
src={selectedTemplate.previewUrl}
alt="Preview"
className="w-full h-full object-cover"
/>
</div>
)}
<p className="text-xs text-green-800 mt-3">
You can customize everything later from your admin dashboard.
</p>
</div>
)}
</div>
);
return (
<div className="min-h-screen bg-gradient-to-b from-green-50 to-white py-12 px-4">
<div className="max-w-3xl mx-auto">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-gray-900 mb-2">Join BudStack</h1>
<p className="text-lg text-gray-600">Launch your medical cannabis dispensary in minutes</p>
</div>
default:
return null;
}
};
case 4:
{
const selectedTemplate = templates.find(
(t) => t.id === formData.templateId,
);
return (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">
Choose Your Store Template
</h3>
<div className="grid grid-cols-2 gap-4">
{templates.map((template) => (
<button
key={template.id}
type="button"
onClick={() =>
setFormData({ ...formData, templateId: template.id })
}
className={`p-4 border-2 rounded-lg text-left transition-all hover:shadow-lg ${
formData.templateId === template.id
? "border-primary bg-primary/5"
: "border-gray-200 hover:border-gray-300"
}`}
>
<div className="flex items-center gap-3 mb-3">
{template.thumbnailUrl ? (
<img
src={template.thumbnailUrl}
alt={template.name}
className="w-12 h-12 object-cover rounded-lg"
/>
) : (
<div className="w-12 h-12 rounded-lg bg-slate-100 flex items-center justify-center">
<Palette className="h-6 w-6 text-slate-400" />
</div>
)}
<div className="flex-1">
<div className="font-semibold">{template.name}</div>
{formData.templateId === template.id && (
<Badge className="mt-1">Selected</Badge>
)}
</div>
</div>
<p className="text-sm text-gray-600 line-clamp-2">
{template.description}
</p>
</button>
))}
</div>
</div>
{selectedTemplate && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg">
<h4 className="text-sm font-semibold text-green-900 mb-2 flex items-center gap-2">
<Eye className="h-4 w-4" />
Preview: {selectedTemplate.name}
</h4>
{selectedTemplate.previewUrl && (
<div className="mt-2 aspect-video rounded-lg overflow-hidden border border-green-100">
<img
src={selectedTemplate.previewUrl}
alt="Preview"
className="w-full h-full object-cover"
/>
</div>
)}
<p className="text-xs text-green-800 mt-3">
You can customize everything later from your admin dashboard.
</p>
</div>
)}
</div>
);
}
default:
return null;
}
};
🧰 Tools
🪛 Biome (2.1.2)

[error] 410-414: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🤖 Prompt for AI Agents
In @nextjs_space/app/onboarding/onboarding-form.tsx around lines 408 - 485, The
variable selectedTemplate declared in case 4 of the switch is not block-scoped
and should be confined to that case; wrap the entire case 4 body in a block
(i.e., add { ... } immediately after case 4) so the const selectedTemplate =
templates.find(...) and the JSX return are inside that block, preventing the
declaration from leaking into other cases; keep the return as-is inside the new
block.

Comment on lines +1 to 8
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { prisma } from "@/lib/db";
import BrandingForm from "./branding-form";
import { Breadcrumbs } from "@/components/admin/shared";

export default async function BrandingPage() {
const session = await getServerSession();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing authOptions in getServerSession() call will break session data access.

The getServerSession() call on line 8 is missing the authOptions parameter. Without it, next-auth won't populate custom session fields like id, tenantId, or role that are configured in your auth callbacks. This will cause session.user.id on line 15 to be undefined, leading to a failed Prisma query.

The customer detail page in this same PR correctly passes authOptions:

const session = await getServerSession(authOptions);
🐛 Proposed fix
 import { getServerSession } from "next-auth";
 import { redirect } from "next/navigation";
+import { authOptions } from "@/lib/auth";
 import { prisma } from "@/lib/db";
 import BrandingForm from "./branding-form";
 import { Breadcrumbs } from "@/components/admin/shared";

 export default async function BrandingPage() {
-  const session = await getServerSession();
+  const session = await getServerSession(authOptions);
🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/branding/page.tsx around lines 1 - 8, The call
to getServerSession() inside BrandingPage is missing the authOptions parameter
so custom session fields (e.g., session.user.id, tenantId, role) are not
populated; update the call to getServerSession(authOptions) using the same
authOptions export used elsewhere (import or reference the existing authOptions)
so session.user.id is defined before using it in the Prisma query and downstream
logic in BrandingPage and BrandingForm.

Comment on lines 10 to 12
if (!session?.user?.email) {
redirect('/auth/login');
redirect("/auth/login");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing role-based authorization - any authenticated user can access this page.

This page only checks that the user is logged in, but doesn't verify they have appropriate roles (TENANT_ADMIN or SUPER_ADMIN). The customer detail page in this PR implements proper role checks:

if (!session || !["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role!)) {
  redirect("/auth/login");
}

Consider adding the same role-based guard here for consistency and security.

🔒 Proposed fix
-  if (!session?.user?.email) {
+  if (
+    !session ||
+    !["TENANT_ADMIN", "SUPER_ADMIN"].includes(session.user.role!)
+  ) {
     redirect("/auth/login");
   }
🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/branding/page.tsx around lines 10 - 12, The
page currently only checks for session?.user?.email and allows any authenticated
user; update the guard to enforce role-based authorization by verifying session
exists and that session.user.role is one of "TENANT_ADMIN" or "SUPER_ADMIN"
(mirroring the customer detail check) and call redirect("/auth/login") when the
check fails; locate the existing session usage and redirect call in page.tsx and
replace the simple email check with the role-inclusive condition using
session.user.role.

Comment on lines +63 to +65
const templatess = await prisma.templates.findMany({
where: { isActive: true, isPublic: true },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Typo in variable name: templatess.

The variable templatess appears to be a typo (double "s"). Consider renaming to baseTemplates or marketplaceTemplates for clarity and to avoid confusion.

🔧 Suggested fix
   // 2. Fetch Available Base Templates (Marketplace)
-  const templatess = await prisma.templates.findMany({
+  const baseTemplates = await prisma.templates.findMany({
     where: { isActive: true, isPublic: true },
   });

Then update references at lines 168 and 227:

-          {templatess.map((template: templates) => (
+          {baseTemplates.map((template: templates) => (
-          {templatess.length === 0 && (
+          {baseTemplates.length === 0 && (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const templatess = await prisma.templates.findMany({
where: { isActive: true, isPublic: true },
});
const baseTemplates = await prisma.templates.findMany({
where: { isActive: true, isPublic: true },
});
🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/templates/page.tsx around lines 63 - 65,
Rename the misspelled variable templatess to a clearer name like baseTemplates
(or marketplaceTemplates) in the template listing statement and update all its
usages (e.g., where templatess is referenced for rendering or logic) to the new
identifier so references at other parts of the file compile and reflect the new
name.

Comment on lines +31 to +35
const posts = await prisma.posts.findMany({
where: { tenantId: user.tenants.id },
orderBy: { createdAt: "desc" },
include: { users: true },
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

fd "schema.prisma" --exec cat {} \;

Repository: AutomatosAI/budstack-saas

Length of output: 18642


Fix the mismatch between query include and component access: relation field is users, not author.

The schema defines the relation field as users (not author). The query correctly includes { users: true }, but if the PostsList component accesses post.author?.name, it will fail at runtime since that property doesn't exist. The component must access post.users?.name instead, or the schema relation field should be renamed to author.

🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/the-wire/page.tsx around lines 31 - 35, The
query uses prisma.posts.findMany including the relation as users (include: {
users: true }) but the consumer component PostsList is accessing post.author
which doesn't exist; update the PostsList rendering to use post.users (e.g.,
post.users?.name) wherever post.author is referenced, or alternatively rename
the relation consistently (either change include to author or rename schema
relation) so the relation symbol matches between prisma.posts.findMany and
PostsList.

Comment on lines 60 to +63
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>(null);
const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>(
null,
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused edit dialog state or implement edit functionality.

The isEditDialogOpen and editingWebhook state variables are initialized but never used. There's no UI or handler to edit existing webhooks, only create and delete operations.

🧹 Proposed fix: Remove unused state
  const [loading, setLoading] = useState(true);
  const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
- const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
- const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>(
-   null,
- );

Alternatively, if edit functionality is planned, implement the edit dialog and wire up the state properly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>(null);
const [editingWebhook, setEditingWebhook] = useState<WebhookData | null>(
null,
);
🤖 Prompt for AI Agents
In @nextjs_space/app/tenant-admin/webhooks/page.tsx around lines 60 - 63, The
two unused state vars isEditDialogOpen/setIsEditDialogOpen and
editingWebhook/setEditingWebhook are declared but never referenced; either
remove these state declarations from the component or implement the edit flow:
add an "Edit" button that calls setEditingWebhook(webhook) and
setIsEditDialogOpen(true), render an EditWebhookDialog that reads editingWebhook
and calls a save handler to update the webhook list, and close the dialog by
calling setIsEditDialogOpen(false) and clearing setEditingWebhook(null). Ensure
all references (isEditDialogOpen, setIsEditDialogOpen, editingWebhook,
setEditingWebhook) are either removed or properly wired into the UI and
handlers.

Comment thread nextjs_space/app/tenant-admin/webhooks/page.tsx
@AutomatosAI AutomatosAI merged commit a13f978 into main Jan 13, 2026
1 check passed
@AutomatosAI AutomatosAI deleted the review/phase5-tenant-admin branch January 15, 2026 21:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants