Skip to content

Admin api - #14

Merged
shakibdshy merged 10 commits into
devfrom
admin-api
Feb 15, 2026
Merged

Admin api#14
shakibdshy merged 10 commits into
devfrom
admin-api

Conversation

@shakibdshy

@shakibdshy shakibdshy commented Feb 15, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added comprehensive admin dashboard with analytics, revenue tracking, order distribution, top performers, and platform health metrics.
    • Introduced server-backed data tables for improved performance on large datasets across admin sections.
    • Enhanced admin management tools for orders, products, users, reviews, and other entities.
  • Improvements

    • Updated review status terminology from "Published" to "Approved" for consistency.

- Add store-specific category types, validators, and query hooks
- Create server functions for fetching categories with proper filtering
- Replace mock data with real API calls in category templates
- Implement category tree building for hierarchical navigation
- Connect product grid to actual product data with loading states
- Add TanStack Query prefetching in route loaders for better UX
- Move VENDOR_STATUS_OPTIONS to shared constants file for reusability
- Create admin-specific entity fetchers with ADMIN_STATUS_OPTIONS
- Add admin category query schemas and types for server-side filtering
- Refactor AdminCategoryTable to use server-side pagination and filtering
- Implement useAdminCategories hook with React Query for state management
- Add admin category server functions with proper authorization middleware
- Replace mock data with real API calls in admin categories page
- Add delete confirmation dialog and mutation state handling
- Add server fetcher for admin brands with filtering, sorting, and pagination
- Replace client-side mock data with server-side data fetching
- Integrate React Query mutations for brand toggle/delete operations
- Add confirmation dialog for brand deletion
- Update components to use server-side pagination pattern consistent with categories
…rations

Add server functions and hooks for admin attribute management with full CRUD capabilities. Introduce server-side pagination, filtering, and sorting through createAdminAttributesFetcher. Replace client-side mock data with real database operations including toggleActive, update, and delete mutations with proper validation and error handling. Enhance AttributeTable to support both admin and shop contexts with improved loading states through AdminAttributeMutationState.
- Add server-side data fetching support to AdminCouponTable with new fetcher prop
- Create use-admin-coupons hook with React Query mutations for toggle/delete
- Implement createAdminCouponsFetcher for standardized data fetching
- Add admin coupon server functions with filtering, sorting, and pagination
- Update admin coupons page to use real mutations with loading states
- Replace mock data with server-driven DataTable implementation
- Replace mock data with server-side fetching for admin orders list
- Implement admin order details page with status update functionality
- Add comprehensive order statistics dashboard with revenue metrics
- Create reusable admin order hooks and server functions
- Extend vendor order components to support admin mode
- Add route for admin order details view
- Add admin product queries with enhanced filtering (stock, status, attributes)
- Create admin product server functions for fetching, updating status, toggling featured, and deleting
- Implement admin products hook with React Query for state management
- Replace mock data with real data fetching in admin products page
- Add product table component with edit/delete actions and loading states
- Integrate confirmation dialog for product deletion
- Replace mock data with server functions and React Query hooks
- Add delete review functionality with confirmation dialog
- Refactor review table columns into separate module
- Update review status terminology from "published" to "approved"
- Add admin review stats and detailed view capabilities
- Replace mock data with server-side fetching using React Query hooks
- Add admin-specific API endpoints and query hooks for tags, taxes, and transactions
- Implement proper pagination, filtering, and sorting for admin tables
- Add transaction statistics dashboard with revenue metrics
- Update table components to support admin context and dynamic data
- Extend type definitions to support admin-specific data structures
…cs components

- Add admin dashboard template with stats cards, charts, and data tables
- Create reusable dashboard components (stats cards, revenue chart, order status chart, etc.)
- Implement data fetching hooks using React Query for real-time analytics
- Add server functions to fetch dashboard statistics from database
- Include utility functions for date calculations and dashboard helpers
- Define TypeScript interfaces for dashboard data types
- Replace placeholder dashboard with full-featured analytics interface
@coderabbitai

coderabbitai Bot commented Feb 15, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch admin-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@shakibdshy
shakibdshy merged commit 72e2216 into dev Feb 15, 2026
1 check passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

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

⚠️ Outside diff range comments (1)
src/lib/validators/shared/product-query.ts (1)

151-159: ⚠️ Potential issue | 🟡 Minor

Line 156: admin filters now allow negative min/max price values.
productBaseFilterFields doesn’t enforce .min(0), so admin queries can accept negative prices. If that’s unintended, reapply the constraint at the admin schema level.

🛡️ Suggested fix
 export const adminProductsQuerySchema = z.object({
   ...paginationFields,
   limit: paginationFields.limit.default(ADMIN_DEFAULT_LIMIT),
   ...sortFields,
   ...searchFields,
   ...productBaseFilterFields,
   ...stockFilterFields,
   ...statusFilterFields,
   ...attributeFilterFields,
+  minPrice: z.coerce.number().min(0).optional(),
+  maxPrice: z.coerce.number().min(0).optional(),
   ...optionalShopIdField,
   ...optionalVendorIdField,
 });
🤖 Fix all issues with AI agents
In `@src/components/containers/shared/reviews/review-table-columns.tsx`:
- Around line 103-108: customerName may be empty which can yield undefined
initials; update the initials generation used inside AvatarFallback (the
expression using customerName.split(" ").map(...).join("").toUpperCase()) to
guard against empty strings by first checking customerName is truthy and
filtering out empty segments (e.g., .split(" ").filter(Boolean)) before mapping
to the first character, and provide a sensible fallback (empty string or
placeholder like "?") when customerName is falsy so AvatarFallback never
receives undefined characters.

In `@src/components/templates/vendor/shop-order-details-template.tsx`:
- Around line 48-55: The component currently computes a default back link using
shopSlug which can be undefined, causing invalid paths like /shop//orders;
update the default backLink logic in the ShopOrderDetailsTemplate (where
shopSlug, mode, and backLink props are used) to be mode-aware: if mode ===
"vendor" require shopSlug (or throw/assert/return a fallback) and build
/shop/{shopSlug}/orders, otherwise when mode === "admin" use the admin orders
path (e.g., /admin/orders) as the default; apply the same mode-aware fix to the
other places referencing shopSlug/backLink (the other default link computations
at the lines you noted) and if you choose to require backLink when shopSlug is
absent, add a clear runtime check that surfaces a helpful error mentioning
shopSlug/backLink and mode.

In `@src/hooks/admin/use-admin-dashboard.tsx`:
- Around line 211-220: The combined isLoading/isError aggregation in
use-admin-dashboard.tsx currently only ORs stats, revenueChart,
orderDistribution, and topShops; update the aggregation to include the remaining
query results (topProducts, lowStock, recentOrders, pendingReviews,
platformHealth) so the combined flags reflect all nine queries — locate the
aggregation where isLoading and isError are computed and add OR checks for
topProducts.isLoading/topProducts.isError, lowStock.isLoading/lowStock.isError,
recentOrders.isLoading/recentOrders.isError,
pendingReviews.isLoading/pendingReviews.isError, and
platformHealth.isLoading/platformHealth.isError (preserving existing variables
names like stats, revenueChart, orderDistribution, topShops).

In `@src/hooks/admin/use-admin-entity-fetchers.ts`:
- Around line 148-154: Remove the unnecessary double cast in the fetcher: inside
fetchFn that calls getAdminOrders, drop the "as unknown as
VendorOrderResponse[]" on the returned data and return the mapped array directly
(data: response.orders ?? []) so TypeScript can infer the correct type;
alternatively ensure getAdminOrders has an explicit return type matching
VendorOrderResponse[] so the fetcher doesn't need any assertion. Keep
references: fetchFn, getAdminOrders, VendorOrderResponse.

In `@src/hooks/admin/use-users.tsx`:
- Around line 86-88: The onSuccess handler currently calls toast.error when a
user is successfully banned; change this to a more appropriate variant (e.g.,
toast.success or toast.warning) so the UI reflects a successful operation while
conveying severity, e.g., replace toast.error("User has been banned") with
toast.success(...) or toast.warning(...), and keep the existing
invalidateUsers() call in the same onSuccess function to refresh state.

In `@src/lib/functions/admin/category.ts`:
- Around line 213-223: The code uses db.$count(categories) when counting
children; replace this with drizzle-orm's standard count() aggregation by
importing count from "drizzle-orm" and changing the select to use .select({
count: count() }) so the childCount query (the block using db.select({ count:
db.$count(categories) }).from(categories).where(eq(categories.parentId, id)))
matches the rest of the codebase and continues to check (childCount?.count ?? 0)
> 0 before throwing the error.

In `@src/lib/functions/admin/dashboard.ts`:
- Around line 66-181: ordersGrowth is computed using orderCountResult[0]?.count
which is the all-time order count, not current-month orders; update the query
that produces orderCountResult to count orders where createdAt >= startOfMonth
(use .where(gte(orders.createdAt, startOfMonth))) so currentMonthOrders reflects
only this month, then keep ordersGrowth calculation as-is using
currentMonthOrders and lastMonthStats[0]?.orderCount for comparison.

In `@src/lib/functions/admin/order.ts`:
- Around line 261-292: The code sets order status/paymentStatus to "refunded"
whenever order.paymentStatus === "paid" even if no refundable payment was found;
modify the flow in the cancel logic (around the payment lookup and createRefund
call in this file) to track whether a refund actually occurred (e.g., a local
boolean like refundSucceeded when createRefund(payment.stripePaymentIntentId)
completes), only update orders.status and orders.paymentStatus to "refunded"
when refundSucceeded is true, and otherwise set status to "cancelled" and leave
paymentStatus unchanged; update the db.update(orders)... call to use that flag
and ensure payments row is only set to "refunded" when the refundSucceeded path
executed.

In `@src/lib/functions/admin/transaction.ts`:
- Around line 98-104: The mapping that builds transactions uses
parseFloat(row.payment.amount) which can produce NaN for null/invalid inputs;
update the transactions mapping (the block that defines amount, applicationFee,
vendorAmount) to validate inputs before parsing: check row.payment.amount and
row.payment.applicationFeeAmount for null/undefined and for numeric strings
(e.g. use Number(value) and isFinite or a regex) and if invalid set amount
and/or applicationFee to null (or a safe fallback like 0 based on business
rules), then compute vendorAmount only when both amount and applicationFee are
valid numbers; ensure the variables amount, applicationFee, and vendorAmount
reflect these validated values.
- Around line 194-201: The pendingPayments query currently sums all pending
payments without the 30-day filter; update the query that builds the pending
payments aggregate (the db.select for total using
COALESCE(SUM(${payments.amount}), 0)) to add a where clause combining
eq(payments.status, "pending") and gte(payments.createdAt, thirtyDaysAgo) (use
and(...) with those two predicates) so it matches the same thirtyDaysAgo time
filter used for revenue, fees, and transaction counts.

In `@src/lib/functions/users.ts`:
- Around line 97-118: The getUsers handler currently hardcodes pagination
(limit:100) and computes total/offset incorrectly; update the createServerFn
handler to accept pagination params (e.g., limit and offset or page) from the
incoming request/context, defaulting limit to 100 and offset to 0, pass those
values into auth.api.listUsers({ query: { limit, offset }, headers:
context.headers }), and use the API's returned total (e.g., result.total)
instead of rawUsers.length; then return users: rawUsers.map(transformUser),
total: result.total, limit, and offset so ListUsersResponse reflects real
paging.

In `@src/lib/helper/tax-rate-query-helpers.ts`:
- Line 134: The field assignment 'isActive: taxRate.isActive || true' forces
true for false values; update the assignment for the taxRate object's isActive
property to only default to true when isActive is null/undefined (not when it is
false) — e.g., replace the `||` fallback with a nullish-coalescing or an
explicit undefined check so that taxRate.isActive stays false when set to false.
- Line 135: The mapping uses the expression "isCompound: taxRate.isCompound ||
false" which incorrectly converts a stored false into false via the || operator;
change it to use nullish coalescing so that only null or undefined fall back
(e.g., use taxRate.isCompound ?? false) — locate the mapping where the taxRate
object is transformed (look for the isCompound property assignment in the
tax-rate mapping function) and replace the || fallback with ?? to preserve
explicit false values.

In `@src/routes/`(admin)/admin/coupons/index.tsx:
- Line 16: The fetcher function is being recreated each render because
createAdminCouponsFetcher() is invoked directly; memoize the fetcher so its
identity is stable (e.g., wrap the call in React's useMemo or useCallback with
the appropriate dependency array) and pass that memoized value to the DataTable
fetcher prop to avoid re-initialization; locate the call to
createAdminCouponsFetcher() in the component and replace it with a memoized
version (reference: createAdminCouponsFetcher, DataTable fetcher prop).

In `@src/routes/`(admin)/admin/reviews/index.tsx:
- Around line 50-61: The handler handleConfirmDelete currently calls
deleteReview.mutate(...) and immediately calls setDeletingReview(null), which
closes ConfirmDeleteDialog before the mutation finishes and prevents the
dialog's isDeleting spinner from showing; update handleConfirmDelete to remove
the immediate setDeletingReview(null) and instead call setDeletingReview(null)
in the mutation callbacks (onSuccess and/or onSettled) provided to
deleteReview.mutate (or configure these callbacks on the deleteReview mutation
itself) so the dialog remains open while deleteReview.isLoading/isDeleting is
true and only closes after the mutation completes.

In `@src/routes/`(admin)/admin/users/index.tsx:
- Around line 31-33: handleAddUser currently awaits createUser(data) but never
closes the AddUserDialog, so the dialog stays open after successful creation;
update handleAddUser (and the similar handler around lines 67-72) to close the
dialog when createUser resolves (and optionally handle errors by keeping it open
and surfacing the error). Specifically, after createUser returns successfully
call the dialog close function (e.g., setIsAddUserOpen(false) or the Close
method provided to AddUserDialog) and ensure you only close on success, not on
caught errors.

In `@src/types/review.ts`:
- Around line 19-35: The BaseReviewFields interface currently types the status
field as a plain string—change its type to the existing ReviewStatus union to
tighten types: update the status property in BaseReviewFields to ReviewStatus,
import or reference the ReviewStatus type where BaseReviewFields is declared,
and adjust any downstream code or tests that assumed a string (e.g., comparisons
or assignments) to use the ReviewStatus values to ensure consistency with the
rest of the codebase.

In `@src/types/users.ts`:
- Around line 9-10: The User type currently has redundant fields banned and
status; remove one to avoid inconsistency. Choose which to keep (prefer keeping
status if you expect more states): delete the banned boolean from the
interface/type and replace any direct reads of user.banned with user.status ===
"banned" (or, if you instead keep banned, remove status and map reads of
user.status to boolean logic). Update any places that construct users to only
set the retained field, adjust serialization/DB mapping and tests accordingly,
and ensure helper functions (e.g., any isBanned checks) derive the value from
the remaining field.
🧹 Nitpick comments (43)
src/lib/functions/admin/tag.ts (2)

53-53: Redundant type assertion.

The inputValidator(adminTagsQuerySchema) already provides type inference for data. The as AdminTagsQuery cast is unnecessary and could mask type mismatches if the schema changes.

♻️ Suggested fix
     } = data as AdminTagsQuery;
+    } = data;

78-78: Prefer fallback defaults over non-null assertions.

While the schema provides defaults for limit and offset, the non-null assertions (!) obscure this contract. Using inline defaults makes the code more defensive and self-documenting.

♻️ Suggested fix
-        return emptyPaginatedResponse<TagItem>(limit!, offset!);
+        return emptyPaginatedResponse<TagItem>(limit ?? 10, offset ?? 0);
src/lib/functions/admin/product.ts (1)

172-177: Consider using .returning() to avoid a separate re-fetch query.

The update followed by a separate select can be consolidated using Drizzle's .returning() clause for better efficiency.

♻️ Proposed refactor
-    await db.update(products).set({ status }).where(eq(products.id, id));
-
-    const [updatedProduct] = await db
-      .select()
-      .from(products)
-      .where(eq(products.id, id));
+    const [updatedProduct] = await db
+      .update(products)
+      .set({ status })
+      .where(eq(products.id, id))
+      .returning();
src/lib/constants/index.ts (1)

1-4: Optional: narrow the option types with a const assertion.
Helps keep value types literal across the app.

♻️ Suggested tweak
 export const VENDOR_STATUS_OPTIONS = [
   { label: "Active", value: "true" },
   { label: "Inactive", value: "false" },
-];
+] as const;
src/components/containers/shared/coupons/coupon-table.tsx (1)

34-43: Consider type constraints to enforce at least one data source.

Both coupons and fetcher are optional, which means neither could be provided. While the fallback to an empty array is safe, a discriminated union or runtime check could make the API clearer.

♻️ Optional: Use a discriminated union for clearer API
// Option 1: Type-level enforcement
type AdminCouponTableProps = CouponTableActions & {
  className?: string;
  mutationState?: CouponMutationState;
  isCouponMutating?: (id: string) => boolean;
  permissions?: CouponPermissions;
} & (
  | { coupons: CouponItem[]; fetcher?: never }
  | { fetcher: (params: DataTableFetchParams) => Promise<DataTableFetchResult<CouponItem>>; coupons?: never }
);
src/lib/utils/dashboard.ts (1)

83-90: Potential edge case: getStartOfWeek may shift months unexpectedly.

When the current date is early in the month (e.g., March 2nd, which is a Monday), subtracting days to reach Sunday may produce a date in the previous month. The function works correctly, but consumers should be aware this can cross month boundaries.

Additionally, the week start is Sunday (US convention). If the codebase needs Monday-start weeks (ISO 8601), this would need adjustment.

src/components/containers/admin/dashboard/top-shops-list.tsx (1)

21-28: Consider reusing formatCurrency from dashboard utilities.

This helper duplicates formatCurrency from src/lib/utils/dashboard.ts. Importing the shared utility would reduce duplication.

♻️ Import from shared utilities
 import { Link } from "@tanstack/react-router";
 import { ExternalLink, Store } from "lucide-react";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { formatCurrency } from "@/lib/utils/dashboard";
 // ... other imports

 export function TopShopsList({ shops, isLoading = false }: TopShopsListProps) {
-  const formatCurrency = (value: number) => {
-    return new Intl.NumberFormat("en-US", {
-      style: "currency",
-      currency: "USD",
-      minimumFractionDigits: 0,
-      maximumFractionDigits: 0,
-    }).format(value);
-  };
-
   const maxRevenue = Math.max(...shops.map((s) => s.revenue), 1);
src/components/containers/admin/dashboard/revenue-chart.tsx (2)

48-55: Consider reusing formatCurrency from dashboard utilities.

This helper is duplicated in multiple dashboard components. Import from src/lib/utils/dashboard.ts for consistency.

♻️ Import shared utility
+import { formatCurrency } from "@/lib/utils/dashboard";
 // ... other imports

 export function RevenueChart({ data, isLoading = false }: RevenueChartProps) {
   // ... loading state ...

-  const formatCurrency = (value: number) => {
-    return new Intl.NumberFormat("en-US", {
-      style: "currency",
-      currency: "USD",
-      minimumFractionDigits: 0,
-      maximumFractionDigits: 0,
-    }).format(value);
-  };
-
   const formatDate = (dateStr: string) => {

83-96: Gradient ID may conflict with multiple chart instances.

The hardcoded id="colorRevenue" could cause issues if multiple RevenueChart components are rendered on the same page. Consider using a unique ID (e.g., via useId hook).

♻️ Use React's useId for unique gradient ID
+import { useId } from "react";

 export function RevenueChart({ data, isLoading = false }: RevenueChartProps) {
+  const gradientId = useId();
   // ...
   
   <defs>
-    <linearGradient id="colorRevenue" x1="0" y1="0" x2="0" y2="1">
+    <linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
       // ...
     </linearGradient>
   </defs>
   // ...
   <Area
     // ...
-    fill="url(`#colorRevenue`)"
+    fill={`url(#${gradientId})`}
   />
src/components/containers/admin/dashboard/top-products-list.tsx (1)

23-30: Use shared currency formatter to avoid duplication.

There’s already a dashboard formatCurrency helper; reusing it keeps formatting consistent and avoids recreating Intl.NumberFormat each render.

♻️ Proposed refactor
-import type { TopProduct } from "@/types/admin-dashboard";
+import type { TopProduct } from "@/types/admin-dashboard";
+import { formatCurrency } from "@/lib/utils/dashboard";
...
-  const formatCurrency = (value: number) => {
-    return new Intl.NumberFormat("en-US", {
-      style: "currency",
-      currency: "USD",
-      minimumFractionDigits: 0,
-      maximumFractionDigits: 0,
-    }).format(value);
-  };
src/components/containers/shared/orders/order-table-columns.tsx (1)

310-523: Consider extracting shared column builders to avoid drift.

createAdminOrderColumns largely mirrors createOrderColumns (status, payment, date, actions). Extracting shared column builders or badge configs would reduce duplication and keep admin/vendor columns in sync.

src/routes/(admin)/admin/products/index.tsx (1)

18-20: Avoid recreating the fetcher on every render.

Creating a new fetcher function each render can cause unnecessary table resets. Consider lifting it to module scope.

♻️ Proposed refactor
+const adminProductsFetcher = createAdminProductsFetcher();
+
 function AdminProductsPage() {
-  const fetcher = createAdminProductsFetcher();
+  const fetcher = adminProductsFetcher;
src/types/taxes.ts (1)

82-90: Consider using PaginatedResponse<NormalizedTaxRate> directly.

TaxRateQueryResult duplicates the exact structure of PaginatedResponse<NormalizedTaxRate>. Since you already import PaginatedResponse and define TaxRateListResponse as an alias, consider making TaxRateQueryResult an alias as well to reduce duplication.

♻️ Suggested simplification
-/**
- * Tax Rate Query Result
- */
-export interface TaxRateQueryResult {
-  data: NormalizedTaxRate[];
-  total: number;
-  limit: number;
-  offset: number;
-}
+/**
+ * Tax Rate Query Result
+ */
+export type TaxRateQueryResult = PaginatedResponse<NormalizedTaxRate>;
src/lib/functions/users.ts (1)

22-32: Avoid any type for better type safety.

The transformUser helper uses any for the input parameter, which bypasses TypeScript's type checking. Consider defining a proper interface for the raw user data from Better Auth.

♻️ Suggested improvement
+interface RawBetterAuthUser {
+  id: string;
+  name?: string | null;
+  email: string;
+  image?: string | null;
+  role?: string | null;
+  banned?: boolean;
+  createdAt: Date | string;
+  updatedAt: Date | string;
+}
+
-const transformUser = (u: any): AdminUser => ({
+const transformUser = (u: RawBetterAuthUser): AdminUser => ({
   id: u.id,
   name: u.name ?? u.email,
   email: u.email,
   image: u.image ?? undefined,
   role: (u.role ?? "customer") as UserRole,
   banned: u.banned ?? false,
   status: u.banned ? "banned" : "active",
   createdAt: u.createdAt,
   updatedAt: u.updatedAt,
 });
src/hooks/admin/use-admin-products.tsx (1)

150-164: Type assertions on mutation variables could be safer.

The type assertions like (deleteProductMutation.variables as string) and (updateStatusMutation.variables?.id as string) could be undefined if the mutation hasn't been called yet. Consider using optional chaining with nullish coalescing to ensure type safety.

♻️ Safer type handling
   const mutationState: AdminProductMutationState = {
     deletingId: deleteProductMutation.isPending
-      ? (deleteProductMutation.variables as string)
+      ? deleteProductMutation.variables ?? null
       : null,
     updatingId: updateStatusMutation.isPending
-      ? (updateStatusMutation.variables?.id as string)
+      ? updateStatusMutation.variables?.id ?? null
       : null,
     togglingId: toggleFeaturedMutation.isPending
-      ? (toggleFeaturedMutation.variables?.id as string)
+      ? toggleFeaturedMutation.variables?.id ?? null
       : null,
     isAnyMutating:
       deleteProductMutation.isPending ||
       updateStatusMutation.isPending ||
       toggleFeaturedMutation.isPending,
   };
src/components/containers/shared/users/add-user-dialog.tsx (1)

45-51: Consider adding password validation.

The password field has no validation rules. Consider adding minimum length and complexity requirements for security.

🛡️ Example with TanStack Form validation
   const form = useForm({
     defaultValues: {
       name: "",
       email: "",
       password: "",
       role: "customer" as UserRole,
     },
+    validators: {
+      onChange: ({ value }) => {
+        const errors: Record<string, string> = {};
+        if (value.password && value.password.length < 8) {
+          errors.password = "Password must be at least 8 characters";
+        }
+        return Object.keys(errors).length > 0 ? errors : undefined;
+      },
+    },
     onSubmit: async ({ value }) => {
src/lib/functions/admin/coupon.ts (1)

179-183: Empty conditional block is a code smell.

The if (existingCoupon.usageCount > 0) block is empty with only a comment. Either implement the soft delete logic, add a warning response, or remove the check entirely to avoid dead code.

♻️ Option 1: Add warning in response
     // Check usage count
     if (existingCoupon.usageCount > 0) {
-      // Soft delete might be preferred, but for now we'll allow deletion
-      // You could add a warning here if needed
+      // Warn that this coupon has been used
+      await db.delete(coupons).where(eq(coupons.id, id));
+      return createSuccessResponse(
+        `Coupon deleted successfully. Note: This coupon had ${existingCoupon.usageCount} uses.`
+      );
     }

     await db.delete(coupons).where(eq(coupons.id, id));
♻️ Option 2: Remove the empty check
-    // Check usage count
-    if (existingCoupon.usageCount > 0) {
-      // Soft delete might be preferred, but for now we'll allow deletion
-      // You could add a warning here if needed
-    }
-
     await db.delete(coupons).where(eq(coupons.id, id));
src/hooks/admin/use-admin-orders.tsx (2)

21-32: Minor: Query key structure differs from other admin hooks.

This file uses a flat ["admin-orders"] base key while use-admin-tags.tsx uses a nested ["admin", "tags"] structure. Consider aligning the pattern for consistency across admin hooks.

 export const adminOrderKeys = {
-  all: ["admin-orders"] as const,
+  all: ["admin", "orders"] as const,
   list: (params?: {

78-131: Missing error handlers in mutations.

Unlike useAdminTagMutations which provides toast notifications on errors, these mutation hooks lack onError callbacks. Consider adding error handling for consistent user feedback.

Proposed fix for useUpdateAdminOrderStatus
     onSuccess: (_, variables) => {
       queryClient.invalidateQueries({ queryKey: adminOrderKeys.all });
       queryClient.invalidateQueries({
         queryKey: adminOrderKeys.detail(variables.orderId),
       });
     },
+    onError: (error: Error) => {
+      toast.error(error.message || "Failed to update order status");
+    },
   });
src/components/containers/admin/dashboard/recent-orders-table.tsx (1)

54-59: Consider reusing existing formatCurrency utility.

A formatCurrency function already exists in src/lib/utils/dashboard.ts. The implementations differ slightly (this one includes decimal places, the utility doesn't), but consider consolidating to avoid duplication. If decimal precision is needed here, the shared utility could be parameterized.

+import { formatCurrency } from "@/lib/utils/dashboard";
+
+// If decimals needed, create a variant or add a parameter to the shared utility
src/components/containers/shared/reviews/review-table-columns.tsx (1)

170-172: "View Details" action has no handler.

The "View Details" menu item is rendered when permissions?.canView is true, but it lacks an onClick handler. Consider either implementing the action or adding a TODO comment.

src/types/transaction.ts (1)

44-54: Consider using a stricter type for status.

TransactionBaseResponse.status is typed as string, but based on the validator in transaction.ts server file (line 29), it should be one of "pending" | "processing" | "succeeded" | "failed" | "refunded". Using a union type would provide better type safety.

♻️ Proposed type refinement
+export type TransactionStatus = "pending" | "processing" | "succeeded" | "failed" | "refunded";
+
 export interface TransactionBaseResponse {
   id: string;
   paymentIntentId: string | null;
   orderId: string;
   orderNumber: string;
   currency: string;
-  status: string;
+  status: TransactionStatus;
   paymentMethod: string;
   provider: string;
   createdAt: string;
 }
src/components/containers/admin/transactions/admin-transactions-table.tsx (1)

136-141: "View Details" action is a placeholder.

Similar to the review table, the "View Details" menu item lacks an onClick handler. Consider implementing navigation to a transaction detail view or adding a TODO.

src/hooks/admin/use-admin-entity-fetchers.ts (1)

36-46: Consider avoiding any for the query type parameter.

Using any for the second generic parameter in createServerFetcher<NormalizedCategory, any> loses type safety. If the query shape is consistent across fetchers, consider defining a shared query type.

src/routes/(admin)/admin/brands/index.tsx (1)

16-16: Consider memoizing the fetcher to prevent unnecessary re-fetches.

createAdminBrandsFetcher() is called on every render, creating a new function reference each time. This may cause the DataTable to re-fetch data on every parent re-render since the fetcher reference changes.

♻️ Proposed fix using useMemo
+import { useMemo } from "react";
...
 function AdminBrandsPage() {
-  const fetcher = createAdminBrandsFetcher();
+  const fetcher = useMemo(() => createAdminBrandsFetcher(), []);
src/routes/(admin)/admin/users/index.tsx (1)

10-13: Missing pendingComponent for route loading state.

Other admin routes (brands, taxes, attributes) include pendingComponent: PageSkeleton in their route configuration for consistent loading UX during route transitions. This route handles loading manually via isLoading check, but the route-level pending state is not covered.

♻️ Proposed fix to add pendingComponent
 export const Route = createFileRoute("/(admin)/admin/users/")({
   component: AdminUsersPage,
+  pendingComponent: PageSkeleton,
 });
src/components/templates/admin/admin-dashboard-template.tsx (2)

33-48: Consider handling error states for dashboard queries.

The dashboard fetches data from 9 different hooks, but none handle error states. If a query fails, the corresponding section will remain in a loading state indefinitely. Consider either:

  1. Adding error boundaries around sections
  2. Passing isError to child components for graceful degradation
  3. Using a consolidated error indicator

85-91: Minor: changeLabel displays "+0 today" when no new users.

When stats?.newUsersToday is 0 or undefined, the label will show "+0 today" which may look odd. Consider conditionally displaying this only when there are new users.

♻️ Optional fix for cleaner display
         <StatsCard
           title="Total Users"
           value={stats?.totalUsers || 0}
-          changeLabel={`+${stats?.newUsersToday || 0} today`}
+          changeLabel={stats?.newUsersToday ? `+${stats.newUsersToday} today` : undefined}
           icon={Users}
           isLoading={statsLoading}
         />
src/routes/(admin)/admin/taxes/index.tsx (1)

16-16: Consider memoizing the fetcher to prevent unnecessary re-fetches.

Same as the brands route, createAdminTaxRatesFetcher() is called on every render. Consider memoizing for consistency and to prevent potential re-fetch issues.

♻️ Proposed fix using useMemo
+import { useMemo } from "react";
...
 function AdminTaxesPage() {
-  const fetcher = createAdminTaxRatesFetcher();
+  const fetcher = useMemo(() => createAdminTaxRatesFetcher(), []);
src/routes/(admin)/admin/attributes/index.tsx (2)

17-17: Consider memoizing the fetcher to prevent unnecessary re-fetches.

Consistent with other admin routes, the fetcher should be memoized to maintain stable reference across renders.

♻️ Proposed fix using useMemo
+import { useMemo } from "react";
...
 function AdminAttributesPage() {
-  const fetcher = createAdminAttributesFetcher();
+  const fetcher = useMemo(() => createAdminAttributesFetcher(), []);

75-99: Redundant handleDialogClose call in onOpenChange.

When the dialog closes (open becomes false), both setIsDialogOpen(open) and handleDialogClose() are called. Looking at useEntityCRUD, handleDialogClose likely already calls setIsDialogOpen(false), resulting in redundant state updates. Additionally, handleDialogClose is called in handleAttributeSubmit on success, which may cause the cleanup to run twice.

♻️ Proposed simplification
       <AddAttributeDialog
         open={isDialogOpen}
-        onOpenChange={(open) => {
-          setIsDialogOpen(open);
-          if (!open) handleDialogClose();
-        }}
+        onOpenChange={setIsDialogOpen}
         onSubmit={handleAttributeSubmit}

If handleDialogClose needs to clear editingAttribute, ensure that's handled appropriately when the dialog closes without submitting.

src/routes/(admin)/admin/categories/index.tsx (1)

16-16: Fetcher recreated on every render — memoize or hoist outside component.

createAdminCategoriesFetcher() is called directly in the component body, creating a new fetcher function instance on each render. This can cause the underlying DataTable to treat it as a new fetcher, potentially triggering unnecessary re-fetches or disrupting caching behavior.

♻️ Proposed fix: memoize the fetcher
+import { useMemo } from "react";
...

 function AdminCategoriesPage() {
-  const fetcher = createAdminCategoriesFetcher();
+  const fetcher = useMemo(() => createAdminCategoriesFetcher(), []);

Alternatively, if the fetcher doesn't depend on component state, hoist it outside the component entirely.

src/hooks/admin/use-admin-attributes.tsx (1)

134-156: Comment claims optimistic updates, but none are implemented.

The comment on line 134 mentions "optimistic updates," but the onMutate handler only cancels queries and returns context for potential rollback — no cache modification occurs. This is technically a "cancel + refetch" pattern rather than true optimistic updates. Consider either implementing actual optimistic cache updates or updating the comment to reflect the current behavior.

src/hooks/admin/use-admin-taxes.tsx (1)

53-61: Consider adding staleTime for consistency with other admin hooks.

Other admin hooks (e.g., use-admin-attributes.tsx) specify a staleTime (30s for lists, 60s for details). This hook omits it, defaulting to 0, which may cause more frequent refetches.

♻️ Proposed fix: add staleTime
   return queryOptions({
     queryKey: adminTaxRatesKeys.list(mergedParams),
     queryFn: () => getAdminTaxRates({ data: mergedParams }),
+    staleTime: 30 * 1000, // 30 seconds
   });
src/hooks/admin/use-admin-categories.tsx (2)

138-151: togglingId conflates two different mutations — may cause incorrect loading indicators.

The togglingId field combines toggleActiveMutation.variables?.id and toggleFeaturedMutation.variables?.id using ||. If both mutations were somehow pending simultaneously (edge case), only one ID would be tracked. More critically, UI consumers cannot distinguish whether the toggle is for isActive or featured status.

Consider splitting into separate tracking fields if the UI needs to differentiate these states.

♻️ Proposed fix: separate tracking for each toggle type
 const mutationState: AdminCategoryMutationState = {
   deletingId: deleteCategoryMutation.isPending
     ? (deleteCategoryMutation.variables as string)
     : null,
-  togglingId:
-    toggleActiveMutation.isPending || toggleFeaturedMutation.isPending
-      ? ((toggleActiveMutation.variables?.id ||
-          toggleFeaturedMutation.variables?.id) as string)
-      : null,
+  togglingActiveId: toggleActiveMutation.isPending
+    ? (toggleActiveMutation.variables?.id ?? null)
+    : null,
+  togglingFeaturedId: toggleFeaturedMutation.isPending
+    ? (toggleFeaturedMutation.variables?.id ?? null)
+    : null,
   isAnyMutating:
     deleteCategoryMutation.isPending ||
     toggleActiveMutation.isPending ||
     toggleFeaturedMutation.isPending,
 };

This would require updating AdminCategoryMutationState and consumers accordingly.


54-72: Consider adding staleTime for consistency.

Similar to the tax rates hook, this lacks explicit staleTime configuration while other admin hooks specify it.

src/components/templates/admin/admin-transactions-template.tsx (1)

85-86: Consider formatting large transaction counts.

stats.totalTransactions is rendered directly without number formatting. For large values (e.g., 1,000,000+), this could be harder to read. Consider using Intl.NumberFormat or a formatting utility for consistency with the currency values.

♻️ Proposed fix
 <div className="font-bold text-2xl">
-  {stats.totalTransactions}
+  {stats.totalTransactions.toLocaleString()}
 </div>
src/hooks/admin/use-admin-reviews.tsx (1)

140-143: Inconsistent mutation return pattern compared to other admin hooks.

Other admin hooks (e.g., useAdminAttributeMutations, useAdminCategoryMutations, useAdminTaxRateMutations) return mutateAsync functions directly along with mutationState and is*Mutating helpers:

return {
  toggleActive: toggleActiveMutation.mutateAsync,
  mutationState,
  isAttributeMutating: (id) => ...
};

This hook returns the full mutation objects instead:

return {
  updateStatus: updateStatusMutation,
  deleteReview: deleteReviewMutation,
};

Consider aligning with the established pattern for consistency across admin hooks.

♻️ Proposed fix: align with other admin hooks
+interface AdminReviewMutationState {
+  updatingStatusId: string | null;
+  deletingId: string | null;
+  isAnyMutating: boolean;
+}

 export function useAdminReviewMutations() {
   const queryClient = useQueryClient();
   // ... existing mutation definitions ...

+  const mutationState: AdminReviewMutationState = {
+    updatingStatusId: updateStatusMutation.isPending
+      ? (updateStatusMutation.variables?.reviewId ?? null)
+      : null,
+    deletingId: deleteReviewMutation.isPending
+      ? (deleteReviewMutation.variables?.reviewId ?? null)
+      : null,
+    isAnyMutating:
+      updateStatusMutation.isPending || deleteReviewMutation.isPending,
+  };

   return {
-    updateStatus: updateStatusMutation,
-    deleteReview: deleteReviewMutation,
+    updateStatus: updateStatusMutation.mutateAsync,
+    deleteReview: deleteReviewMutation.mutateAsync,
+    isUpdatingStatus: updateStatusMutation.isPending,
+    isDeleting: deleteReviewMutation.isPending,
+    mutationState,
+    isReviewMutating: (reviewId: string) =>
+      mutationState.updatingStatusId === reviewId ||
+      mutationState.deletingId === reviewId,
   };
 }
src/components/containers/shared/users/user-table.tsx (2)

157-165: Hardcoded role values could drift from the UserRole type.

The role options are hardcoded as string literals. If UserRole is updated, this menu won't reflect the changes and may cause runtime issues.

♻️ Consider deriving options from UserRole type

If UserRole is a union type or enum, consider deriving the options programmatically:

// If UserRole is defined as: type UserRole = "customer" | "vendor" | "admin"
const ROLE_OPTIONS: { value: UserRole; label: string }[] = [
  { value: "customer", label: "Customer" },
  { value: "vendor", label: "Vendor" },
  { value: "admin", label: "Admin" },
];

Then map over ROLE_OPTIONS in the JSX. This keeps the source of truth in one place.


100-113: Status badge handles only "banned" and "active" states.

The badge logic applies destructive styling for "banned" and green for "active", but any other status value would fall through to the default variant without specific styling. If additional statuses exist (e.g., "suspended", "pending"), they may need handling.

src/routes/(admin)/admin/reviews/index.tsx (1)

52-54: Consider replacing window.prompt() with a proper dialog input.

window.prompt() provides a poor user experience—it's blocking, cannot be styled, and looks inconsistent across browsers. A custom dialog component with an input field would provide better UX and allow proper validation feedback.

src/lib/functions/admin/review.ts (1)

212-213: Replace console.log with proper audit logging.

The comment acknowledges this should be stored in production. Using console.log for admin deletion audit trails has several issues:

  • Logs are ephemeral and may be lost
  • No structured format for querying/analysis
  • PII (deletion reason) in logs without proper handling

Consider storing to an audit table or using a structured logging service.

♻️ Suggested approach
-    // Log deletion reason (in production, you might want to store this)
-    console.log(`Admin deleted review ${reviewId}. Reason: ${reason}`);
+    // TODO: Store deletion audit to database or structured logging service
+    // Example: await db.insert(auditLogs).values({
+    //   action: 'REVIEW_DELETED',
+    //   entityId: reviewId,
+    //   reason,
+    //   adminId: ctx.user.id, // if available from middleware context
+    //   timestamp: new Date(),
+    // });
src/hooks/admin/use-users.tsx (1)

56-61: Remove as any type cast and align role types properly.

The role parameter on lines 60 and 117 is cast to any, which bypasses type safety. Since UserRole is already properly typed as "admin" | "vendor" | "customer", consider:

  1. Checking if better-auth's admin client supports the UserRole type directly without casting
  2. If not, create a type-safe mapping function instead of using as any
  3. Verify the admin client's role parameter type in better-auth's type definitions and update accordingly

Comment on lines +103 to +108
{customerName
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</AvatarFallback>

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

Potential runtime error if customerName is empty.

If customerName is an empty string, n[0] will be undefined for empty segments, which will still work but may produce unexpected initials. Consider adding a guard.

🛡️ Proposed defensive fix
               <AvatarFallback className="text-xs">
                 {customerName
                   .split(" ")
-                  .map((n) => n[0])
+                  .filter(Boolean)
+                  .map((n) => n[0] ?? "")
                   .join("")
                   .toUpperCase()}
               </AvatarFallback>
📝 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
{customerName
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()}
</AvatarFallback>
{customerName
.split(" ")
.filter(Boolean)
.map((n) => n[0] ?? "")
.join("")
.toUpperCase()}
</AvatarFallback>
🤖 Prompt for AI Agents
In `@src/components/containers/shared/reviews/review-table-columns.tsx` around
lines 103 - 108, customerName may be empty which can yield undefined initials;
update the initials generation used inside AvatarFallback (the expression using
customerName.split(" ").map(...).join("").toUpperCase()) to guard against empty
strings by first checking customerName is truthy and filtering out empty
segments (e.g., .split(" ").filter(Boolean)) before mapping to the first
character, and provide a sensible fallback (empty string or placeholder like
"?") when customerName is falsy so AvatarFallback never receives undefined
characters.

Comment on lines +48 to +55
shopSlug?: string;
order: VendorOrderDetailResponse;
mode?: "vendor" | "admin";
backLink?: {
to: string;
params?: Record<string, string>;
label?: string;
};

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

Prevent invalid back links when shopSlug is absent.

With shopSlug optional, the default link can resolve to /shop//orders when admin mode doesn’t supply backLink. Consider making the default link mode-aware (or requiring backLink when shopSlug is missing).

🛠️ Proposed fix (mode-aware default)
-  const resolvedBackLink = backLink ?? {
-    to: "/shop/$slug/orders",
-    params: { slug: shopSlug ?? "" },
-    label: "Back to Orders",
-  };
+  const resolvedBackLink =
+    backLink ??
+    (mode === "admin"
+      ? { to: "/admin/orders", label: "Back to Orders" }
+      : {
+          to: "/shop/$slug/orders",
+          params: { slug: shopSlug ?? "" },
+          label: "Back to Orders",
+        });

Also applies to: 61-62, 135-139, 145-148

🤖 Prompt for AI Agents
In `@src/components/templates/vendor/shop-order-details-template.tsx` around lines
48 - 55, The component currently computes a default back link using shopSlug
which can be undefined, causing invalid paths like /shop//orders; update the
default backLink logic in the ShopOrderDetailsTemplate (where shopSlug, mode,
and backLink props are used) to be mode-aware: if mode === "vendor" require
shopSlug (or throw/assert/return a fallback) and build /shop/{shopSlug}/orders,
otherwise when mode === "admin" use the admin orders path (e.g., /admin/orders)
as the default; apply the same mode-aware fix to the other places referencing
shopSlug/backLink (the other default link computations at the lines you noted)
and if you choose to require backLink when shopSlug is absent, add a clear
runtime check that surfaces a helpful error mentioning shopSlug/backLink and
mode.

Comment on lines +211 to +220
isLoading:
stats.isLoading ||
revenueChart.isLoading ||
orderDistribution.isLoading ||
topShops.isLoading,
isError:
stats.isError ||
revenueChart.isError ||
orderDistribution.isError ||
topShops.isError,

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

isLoading and isError don't aggregate all queries.

The combined states only check 4 of 9 queries (stats, revenueChart, orderDistribution, topShops). If topProducts, lowStock, recentOrders, pendingReviews, or platformHealth are loading or errored, the combined isLoading/isError will incorrectly report false.

🛠️ Proposed fix to include all queries
     isLoading:
       stats.isLoading ||
       revenueChart.isLoading ||
       orderDistribution.isLoading ||
-      topShops.isLoading,
+      topShops.isLoading ||
+      topProducts.isLoading ||
+      lowStock.isLoading ||
+      recentOrders.isLoading ||
+      pendingReviews.isLoading ||
+      platformHealth.isLoading,
     isError:
       stats.isError ||
       revenueChart.isError ||
       orderDistribution.isError ||
-      topShops.isError,
+      topShops.isError ||
+      topProducts.isError ||
+      lowStock.isError ||
+      recentOrders.isError ||
+      pendingReviews.isError ||
+      platformHealth.isError,
📝 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
isLoading:
stats.isLoading ||
revenueChart.isLoading ||
orderDistribution.isLoading ||
topShops.isLoading,
isError:
stats.isError ||
revenueChart.isError ||
orderDistribution.isError ||
topShops.isError,
isLoading:
stats.isLoading ||
revenueChart.isLoading ||
orderDistribution.isLoading ||
topShops.isLoading ||
topProducts.isLoading ||
lowStock.isLoading ||
recentOrders.isLoading ||
pendingReviews.isLoading ||
platformHealth.isLoading,
isError:
stats.isError ||
revenueChart.isError ||
orderDistribution.isError ||
topShops.isError ||
topProducts.isError ||
lowStock.isError ||
recentOrders.isError ||
pendingReviews.isError ||
platformHealth.isError,
🤖 Prompt for AI Agents
In `@src/hooks/admin/use-admin-dashboard.tsx` around lines 211 - 220, The combined
isLoading/isError aggregation in use-admin-dashboard.tsx currently only ORs
stats, revenueChart, orderDistribution, and topShops; update the aggregation to
include the remaining query results (topProducts, lowStock, recentOrders,
pendingReviews, platformHealth) so the combined flags reflect all nine queries —
locate the aggregation where isLoading and isError are computed and add OR
checks for topProducts.isLoading/topProducts.isError,
lowStock.isLoading/lowStock.isError,
recentOrders.isLoading/recentOrders.isError,
pendingReviews.isLoading/pendingReviews.isError, and
platformHealth.isLoading/platformHealth.isError (preserving existing variables
names like stats, revenueChart, orderDistribution, topShops).

Comment on lines +148 to +154
fetchFn: async (query) => {
const response = await getAdminOrders({ data: query });
return {
data: (response.orders ?? []) as unknown as VendorOrderResponse[],
total: response.total ?? 0,
};
},

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:

#!/bin/bash
# Check the return type of getAdminOrders
ast-grep --pattern 'export const getAdminOrders = createServerFn($$$).handler(async ($$$): Promise<$_> {'

Repository: FullStack-Flow/shop-stack

Length of output: 51


🏁 Script executed:

# Also search for the function definition more broadly
rg -A 5 'getAdminOrders\s*=' --type ts

Repository: FullStack-Flow/shop-stack

Length of output: 484


🏁 Script executed:

# Find VendorOrderResponse type definition
rg 'type VendorOrderResponse|interface VendorOrderResponse' --type ts

Repository: FullStack-Flow/shop-stack

Length of output: 127


🏁 Script executed:

# Check the response structure from getAdminOrders
rg 'getAdminOrders' src/hooks/admin/use-admin-entity-fetchers.ts -B 2 -A 8

Repository: FullStack-Flow/shop-stack

Length of output: 1006


🏁 Script executed:

# Get the full implementation of getAdminOrders
cat src/lib/functions/admin/order.ts | head -80

Repository: FullStack-Flow/shop-stack

Length of output: 2594


🏁 Script executed:

# Get VendorOrderResponse type definition
rg -A 15 'export interface VendorOrderResponse' src/types/orders.ts

Repository: FullStack-Flow/shop-stack

Length of output: 445


🏁 Script executed:

# Also check what type the handler returns explicitly
ast-grep --pattern 'getAdminOrders = createServerFn($$$).handler(async ($$$) => {
  $$$
  return $_
})'

Repository: FullStack-Flow/shop-stack

Length of output: 51


🏁 Script executed:

# Get more of the getAdminOrders return statement
cat src/lib/functions/admin/order.ts | tail -50

Repository: FullStack-Flow/shop-stack

Length of output: 1290


🏁 Script executed:

# Get complete VendorOrderResponse definition
rg -A 25 'export interface VendorOrderResponse' src/types/orders.ts

Repository: FullStack-Flow/shop-stack

Length of output: 604


🏁 Script executed:

# Get the complete getAdminOrders handler - search around line that was shown
rg -A 100 'export const getAdminOrders' src/lib/functions/admin/order.ts | head -150

Repository: FullStack-Flow/shop-stack

Length of output: 3026


🏁 Script executed:

# Search specifically for the return statement in getAdminOrders
rg -A 50 'orders: adminOrders.map' src/lib/functions/admin/order.ts

Repository: FullStack-Flow/shop-stack

Length of output: 1816


Remove the unnecessary double type assertion; the mapped object already matches VendorOrderResponse.

The getAdminOrders function returns a properly shaped object that exactly matches VendorOrderResponse. The double assertion as unknown as VendorOrderResponse[] is redundant and masks the actual type from TypeScript. Remove the assertion and let TypeScript infer the correct type from the mapping function, or add an explicit return type annotation to getAdminOrders to make the contract clear.

🤖 Prompt for AI Agents
In `@src/hooks/admin/use-admin-entity-fetchers.ts` around lines 148 - 154, Remove
the unnecessary double cast in the fetcher: inside fetchFn that calls
getAdminOrders, drop the "as unknown as VendorOrderResponse[]" on the returned
data and return the mapped array directly (data: response.orders ?? []) so
TypeScript can infer the correct type; alternatively ensure getAdminOrders has
an explicit return type matching VendorOrderResponse[] so the fetcher doesn't
need any assertion. Keep references: fetchFn, getAdminOrders,
VendorOrderResponse.

Comment on lines +86 to +88
onSuccess: () => {
toast.error("User has been banned");
invalidateUsers();

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

Use appropriate toast variant for successful ban operation.

toast.error is used for a successful ban, which may confuse users into thinking the operation failed. Consider using toast.success or toast.warning to indicate the action completed while conveying its severity.

🔧 Suggested fix
     onSuccess: () => {
-      toast.error("User has been banned");
+      toast.warning("User has been banned");
       invalidateUsers();
     },
📝 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
onSuccess: () => {
toast.error("User has been banned");
invalidateUsers();
onSuccess: () => {
toast.warning("User has been banned");
invalidateUsers();
🤖 Prompt for AI Agents
In `@src/hooks/admin/use-users.tsx` around lines 86 - 88, The onSuccess handler
currently calls toast.error when a user is successfully banned; change this to a
more appropriate variant (e.g., toast.success or toast.warning) so the UI
reflects a successful operation while conveying severity, e.g., replace
toast.error("User has been banned") with toast.success(...) or
toast.warning(...), and keep the existing invalidateUsers() call in the same
onSuccess function to refresh state.


function AdminCouponsPage() {
const [coupons, setCoupons] = useState<Coupon[]>(mockCoupons);
const fetcher = createAdminCouponsFetcher();

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

Memoize the fetcher to prevent unnecessary re-renders.

createAdminCouponsFetcher() is called on every render, creating a new function reference. This causes the DataTable to re-initialize on each render since the fetcher prop identity changes.

🛠️ Proposed fix
+import { useMemo } from "react";
+
 function AdminCouponsPage() {
-  const fetcher = createAdminCouponsFetcher();
+  const fetcher = useMemo(() => createAdminCouponsFetcher(), []);
📝 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 fetcher = createAdminCouponsFetcher();
import { useMemo } from "react";
function AdminCouponsPage() {
const fetcher = useMemo(() => createAdminCouponsFetcher(), []);
🤖 Prompt for AI Agents
In `@src/routes/`(admin)/admin/coupons/index.tsx at line 16, The fetcher function
is being recreated each render because createAdminCouponsFetcher() is invoked
directly; memoize the fetcher so its identity is stable (e.g., wrap the call in
React's useMemo or useCallback with the appropriate dependency array) and pass
that memoized value to the DataTable fetcher prop to avoid re-initialization;
locate the call to createAdminCouponsFetcher() in the component and replace it
with a memoized version (reference: createAdminCouponsFetcher, DataTable fetcher
prop).

Comment on lines +50 to 61
const handleConfirmDelete = () => {
if (!deletingReview) return;
const reason = window.prompt(
"Provide a reason for deletion (min 5 characters).",
);
if (!reason || reason.trim().length < 5) {
toast.error("Deletion reason must be at least 5 characters.");
return;
}
deleteReview.mutate({ reviewId: deletingReview.id, reason: reason.trim() });
setDeletingReview(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

Dialog closes before mutation completes, hiding the loading state.

setDeletingReview(null) on Line 60 closes the ConfirmDeleteDialog immediately after calling mutate(), before the deletion completes. This means users won't see the "Deleting..." spinner defined in the dialog's isDeleting prop.

🛠️ Proposed fix using mutation callbacks
   const handleConfirmDelete = () => {
     if (!deletingReview) return;
     const reason = window.prompt(
       "Provide a reason for deletion (min 5 characters).",
     );
     if (!reason || reason.trim().length < 5) {
       toast.error("Deletion reason must be at least 5 characters.");
       return;
     }
-    deleteReview.mutate({ reviewId: deletingReview.id, reason: reason.trim() });
-    setDeletingReview(null);
+    deleteReview.mutate(
+      { reviewId: deletingReview.id, reason: reason.trim() },
+      {
+        onSettled: () => setDeletingReview(null),
+      }
+    );
   };
📝 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 handleConfirmDelete = () => {
if (!deletingReview) return;
const reason = window.prompt(
"Provide a reason for deletion (min 5 characters).",
);
if (!reason || reason.trim().length < 5) {
toast.error("Deletion reason must be at least 5 characters.");
return;
}
deleteReview.mutate({ reviewId: deletingReview.id, reason: reason.trim() });
setDeletingReview(null);
};
const handleConfirmDelete = () => {
if (!deletingReview) return;
const reason = window.prompt(
"Provide a reason for deletion (min 5 characters).",
);
if (!reason || reason.trim().length < 5) {
toast.error("Deletion reason must be at least 5 characters.");
return;
}
deleteReview.mutate(
{ reviewId: deletingReview.id, reason: reason.trim() },
{
onSettled: () => setDeletingReview(null),
}
);
};
🤖 Prompt for AI Agents
In `@src/routes/`(admin)/admin/reviews/index.tsx around lines 50 - 61, The handler
handleConfirmDelete currently calls deleteReview.mutate(...) and immediately
calls setDeletingReview(null), which closes ConfirmDeleteDialog before the
mutation finishes and prevents the dialog's isDeleting spinner from showing;
update handleConfirmDelete to remove the immediate setDeletingReview(null) and
instead call setDeletingReview(null) in the mutation callbacks (onSuccess and/or
onSettled) provided to deleteReview.mutate (or configure these callbacks on the
deleteReview mutation itself) so the dialog remains open while
deleteReview.isLoading/isDeleting is true and only closes after the mutation
completes.

Comment on lines +31 to +33
const handleAddUser = async (data: AdminUserFormValues) => {
await createUser(data);
};

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

Add dialog close on successful user creation.

handleAddUser calls createUser but doesn't close the AddUserDialog on success. The user will need to manually close the dialog after adding a user, which degrades the UX.

🐛 Proposed fix to close dialog on success
   const handleAddUser = async (data: AdminUserFormValues) => {
     await createUser(data);
+    setIsAddDialogOpen(false);
   };

Also applies to: 67-72

🤖 Prompt for AI Agents
In `@src/routes/`(admin)/admin/users/index.tsx around lines 31 - 33, handleAddUser
currently awaits createUser(data) but never closes the AddUserDialog, so the
dialog stays open after successful creation; update handleAddUser (and the
similar handler around lines 67-72) to close the dialog when createUser resolves
(and optionally handle errors by keeping it open and surfacing the error).
Specifically, after createUser returns successfully call the dialog close
function (e.g., setIsAddUserOpen(false) or the Close method provided to
AddUserDialog) and ensure you only close on success, not on caught errors.

Comment thread src/types/review.ts
Comment on lines +19 to +35
export interface BaseReviewFields {
id: string;
userId: string;
userName: string;
userAvatar: string | null;
productId: string;
productName: string;
rating: number;
title: string;
comment: string;
status: string;
helpfulCount: number;
isVerifiedPurchase: boolean;
vendorResponse: string | null;
vendorRespondedAt: string | null;
createdAt: string;
updatedAt: string;

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

Narrow BaseReviewFields.status to ReviewStatus for consistency.

Right now it’s a plain string, which weakens type safety relative to the new status union used elsewhere.

🔧 Proposed fix
 export interface BaseReviewFields {
   id: string;
   userId: string;
   userName: string;
   userAvatar: string | null;
   productId: string;
   productName: string;
   rating: number;
   title: string;
   comment: string;
-  status: string;
+  status: ReviewStatus;
   helpfulCount: number;
   isVerifiedPurchase: boolean;
   vendorResponse: string | null;
   vendorRespondedAt: string | null;
   createdAt: string;
   updatedAt: string;
 }
📝 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
export interface BaseReviewFields {
id: string;
userId: string;
userName: string;
userAvatar: string | null;
productId: string;
productName: string;
rating: number;
title: string;
comment: string;
status: string;
helpfulCount: number;
isVerifiedPurchase: boolean;
vendorResponse: string | null;
vendorRespondedAt: string | null;
createdAt: string;
updatedAt: string;
export interface BaseReviewFields {
id: string;
userId: string;
userName: string;
userAvatar: string | null;
productId: string;
productName: string;
rating: number;
title: string;
comment: string;
status: ReviewStatus;
helpfulCount: number;
isVerifiedPurchase: boolean;
vendorResponse: string | null;
vendorRespondedAt: string | null;
createdAt: string;
updatedAt: string;
}
🤖 Prompt for AI Agents
In `@src/types/review.ts` around lines 19 - 35, The BaseReviewFields interface
currently types the status field as a plain string—change its type to the
existing ReviewStatus union to tighten types: update the status property in
BaseReviewFields to ReviewStatus, import or reference the ReviewStatus type
where BaseReviewFields is declared, and adjust any downstream code or tests that
assumed a string (e.g., comparisons or assignments) to use the ReviewStatus
values to ensure consistency with the rest of the codebase.

Comment thread src/types/users.ts
Comment on lines +9 to +10
banned: boolean;
status: "active" | "banned";

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

Redundant fields: banned and status convey the same information.

The banned: boolean field and status: "active" | "banned" field are semantically equivalent — one can be derived from the other. This creates a risk of inconsistency if they fall out of sync.

Consider removing one field. If status might expand to include more values in the future (e.g., "suspended"), keep status and derive banned where needed. Otherwise, banned alone may suffice.

♻️ Option A: Keep only status
 export interface AdminUser {
   id: string;
   name: string;
   email: string;
   image?: string;
   role: UserRole;
-  banned: boolean;
   status: "active" | "banned";
   createdAt: Date | string;
   updatedAt: Date | string;
 }
♻️ Option B: Keep only banned
 export interface AdminUser {
   id: string;
   name: string;
   email: string;
   image?: string;
   role: UserRole;
   banned: boolean;
-  status: "active" | "banned";
   createdAt: Date | string;
   updatedAt: Date | string;
 }
📝 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
banned: boolean;
status: "active" | "banned";
export interface AdminUser {
id: string;
name: string;
email: string;
image?: string;
role: UserRole;
status: "active" | "banned";
createdAt: Date | string;
updatedAt: Date | string;
}
Suggested change
banned: boolean;
status: "active" | "banned";
export interface AdminUser {
id: string;
name: string;
email: string;
image?: string;
role: UserRole;
banned: boolean;
createdAt: Date | string;
updatedAt: Date | string;
}
🤖 Prompt for AI Agents
In `@src/types/users.ts` around lines 9 - 10, The User type currently has
redundant fields banned and status; remove one to avoid inconsistency. Choose
which to keep (prefer keeping status if you expect more states): delete the
banned boolean from the interface/type and replace any direct reads of
user.banned with user.status === "banned" (or, if you instead keep banned,
remove status and map reads of user.status to boolean logic). Update any places
that construct users to only set the retained field, adjust serialization/DB
mapping and tests accordingly, and ensure helper functions (e.g., any isBanned
checks) derive the value from the remaining field.

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.

1 participant