From cf108a5cee351c30dce6e3cd5a7c4ff1d3f74698 Mon Sep 17 00:00:00 2001
From: israel <israeliyanda5@gmail.com>
Date: Fri, 10 Apr 2026 12:39:32 +0100
Subject: [PATCH 1/5] fix(platform): refine toast icons, CSV header detection,
 and vendor bulk delete
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Simplify toaster to always show variant icons (info default) instead of
  conditional icon/no-icon layout
- Remove explicit 'success' variant from delete/revoke toasts (default handles it)
- Replace brittle heuristic-based CSV header detection with first-row-is-header
  approach and drop the COMMON_HEADER_NAMES dictionary
- Unify product import mappers: rename excel→record, make CSV fallback return null
  so headerless files fail clearly instead of silently misaligning columns
- Add expectedColumns hints to customer and vendor import forms
- Add bulk delete support to vendors table with row selection
---
 .../ui/entity/entity-delete-dialog.tsx        |  1 -
 .../app/components/ui/feedback/toaster.tsx    | 54 +++++++------------
 .../agents/components/agent-delete-dialog.tsx |  1 -
 .../components/customer-import-form.tsx       |  1 +
 .../components/products-import-dialog.tsx     | 21 ++------
 .../components/api-key-revoke-dialog.tsx      |  1 -
 .../providers/components/providers-table.tsx  |  2 +-
 .../teams/components/team-delete-dialog.tsx   |  1 -
 .../vendors/components/vendor-import-form.tsx |  2 +-
 .../vendors/components/vendors-table.tsx      | 29 +++++++++-
 .../hooks/use-vendors-table-config.tsx        |  1 +
 .../hooks/__tests__/use-file-import.test.ts   | 12 ++---
 .../platform/app/hooks/use-file-import.ts     | 35 ++++++------
 services/platform/lib/utils/file-parsing.ts   | 51 +-----------------
 services/platform/messages/de.json            |  7 +--
 services/platform/messages/en.json            |  7 +--
 16 files changed, 91 insertions(+), 135 deletions(-)

diff --git a/services/platform/app/components/ui/entity/entity-delete-dialog.tsx b/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
index 42dfd62b1e..d96bbfc239 100644
--- a/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
+++ b/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
@@ -80,7 +80,6 @@ export function EntityDeleteDialog<TEntity>({
       await deleteMutation(entity);
       toast({
         title: translations.successMessage,
-        variant: 'success',
       });
       onClose();
       onSuccess?.();
diff --git a/services/platform/app/components/ui/feedback/toaster.tsx b/services/platform/app/components/ui/feedback/toaster.tsx
index 3ab4542a17..1c9c0cfbbe 100644
--- a/services/platform/app/components/ui/feedback/toaster.tsx
+++ b/services/platform/app/components/ui/feedback/toaster.tsx
@@ -2,7 +2,7 @@
 
 import * as ToastPrimitives from '@radix-ui/react-toast';
 import { cva } from 'class-variance-authority';
-import { X, CheckCircle2, XCircle } from 'lucide-react';
+import { X, CheckCircle2, XCircle, Info } from 'lucide-react';
 
 import { useToast } from '@/app/hooks/use-toast';
 import { cn } from '@/lib/utils/cn';
@@ -34,7 +34,9 @@ function VariantIcon({ variant }: { variant?: ToastVariant }) {
     case 'destructive':
       return <XCircle className="text-destructive size-5" aria-hidden="true" />;
     default:
-      return null;
+      return (
+        <Info className="text-muted-foreground size-5" aria-hidden="true" />
+      );
   }
 }
 
@@ -45,47 +47,29 @@ export function Toaster() {
     <ToastPrimitives.Provider duration={3500}>
       {toasts.map(
         ({ id, title, description, action, variant, className, ...props }) => {
-          const icon = <VariantIcon variant={variant} />;
-          const hasIcon = variant === 'success' || variant === 'destructive';
-
           return (
             <ToastPrimitives.Root
               key={id}
               className={cn(toastVariants({ variant }), className)}
               {...props}
             >
-              {hasIcon ? (
-                <div className="flex items-start space-x-3">
-                  {icon}
-                  <div className="flex-1 pr-4">
-                    <div className="grid gap-1">
-                      {title && (
-                        <ToastPrimitives.Title className="text-sm font-semibold">
-                          {title}
-                        </ToastPrimitives.Title>
-                      )}
-                      {description && (
-                        <ToastPrimitives.Description className="text-sm opacity-90">
-                          {description}
-                        </ToastPrimitives.Description>
-                      )}
-                    </div>
+              <div className="flex items-start space-x-3">
+                <VariantIcon variant={variant} />
+                <div className="flex-1 pr-4">
+                  <div className="grid gap-1">
+                    {title && (
+                      <ToastPrimitives.Title className="text-sm font-semibold">
+                        {title}
+                      </ToastPrimitives.Title>
+                    )}
+                    {description && (
+                      <ToastPrimitives.Description className="text-sm opacity-90">
+                        {description}
+                      </ToastPrimitives.Description>
+                    )}
                   </div>
                 </div>
-              ) : (
-                <div className="grid gap-1">
-                  {title && (
-                    <ToastPrimitives.Title className="text-sm font-semibold">
-                      {title}
-                    </ToastPrimitives.Title>
-                  )}
-                  {description && (
-                    <ToastPrimitives.Description className="text-sm opacity-90">
-                      {description}
-                    </ToastPrimitives.Description>
-                  )}
-                </div>
-              )}
+              </div>
               {action}
               <ToastPrimitives.Close
                 className="text-foreground/50 hover:text-foreground absolute top-2.5 right-2.5 rounded-md p-1 opacity-0 transition-opacity group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 focus:opacity-100 focus:ring-2 focus:outline-none group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600"
diff --git a/services/platform/app/features/agents/components/agent-delete-dialog.tsx b/services/platform/app/features/agents/components/agent-delete-dialog.tsx
index 2cc85a66ea..b0dd337342 100644
--- a/services/platform/app/features/agents/components/agent-delete-dialog.tsx
+++ b/services/platform/app/features/agents/components/agent-delete-dialog.tsx
@@ -38,7 +38,6 @@ export function AgentDeleteDialog({
       });
       toast({
         title: t('agents.agentDeleted'),
-        variant: 'success',
       });
       onOpenChange(false);
       onDeleted?.();
diff --git a/services/platform/app/features/customers/components/customer-import-form.tsx b/services/platform/app/features/customers/components/customer-import-form.tsx
index e47a983147..88e9ae1bc4 100644
--- a/services/platform/app/features/customers/components/customer-import-form.tsx
+++ b/services/platform/app/features/customers/components/customer-import-form.tsx
@@ -169,6 +169,7 @@ export function CustomerImportForm({
           </FileUpload.Root>
           <Text as="div" variant="caption" className="leading-relaxed">
             <ul className="list-outside list-disc space-y-2 pl-4">
+              <li>{t('importForm.expectedColumns')}</li>
               <li>{t('importForm.localeHint')}</li>
               <li className="text-blue-600">{t('importForm.statusNote')}</li>
             </ul>
diff --git a/services/platform/app/features/products/components/products-import-dialog.tsx b/services/platform/app/features/products/components/products-import-dialog.tsx
index df56eda120..b37b64b2a3 100644
--- a/services/platform/app/features/products/components/products-import-dialog.tsx
+++ b/services/platform/app/features/products/components/products-import-dialog.tsx
@@ -77,9 +77,9 @@ export function ProductsImportDialog({
     [],
   );
 
-  const excelMapper = useCallback(
+  const recordMapper = useCallback(
     (record: Record<string, unknown>): ParsedProduct | null => {
-      const result = productMappers.excel(record);
+      const result = productMappers.record(record);
       if (!result) return null;
 
       return {
@@ -90,22 +90,9 @@ export function ProductsImportDialog({
     [validateStatus],
   );
 
-  const csvMapper = useCallback(
-    (row: string[], index: number): ParsedProduct | null => {
-      const result = productMappers.csv(row, index);
-      if (!result) return null;
-
-      return {
-        ...result,
-        status: validateStatus(row[7]),
-      };
-    },
-    [validateStatus],
-  );
-
   const { parseFile } = useFileImport<ParsedProduct>({
-    csvMapper,
-    excelMapper,
+    csvMapper: productMappers.csv,
+    excelMapper: recordMapper,
   });
 
   const resetForm = useCallback(() => {
diff --git a/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx b/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
index fcea4c55fc..5390bd14a4 100644
--- a/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
+++ b/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
@@ -35,7 +35,6 @@ export function ApiKeyRevokeDialog({
       onSuccess: () => {
         toast({
           title: tSettings('apiKeys.keyRevoked'),
-          variant: 'success',
         });
         onOpenChange(false);
         onSuccess?.();
diff --git a/services/platform/app/features/settings/providers/components/providers-table.tsx b/services/platform/app/features/settings/providers/components/providers-table.tsx
index bfc65ca1fd..2e2edb2677 100644
--- a/services/platform/app/features/settings/providers/components/providers-table.tsx
+++ b/services/platform/app/features/settings/providers/components/providers-table.tsx
@@ -90,7 +90,7 @@ export function ProvidersTable({ organizationId }: ProvidersTableProps) {
         orgSlug: 'default',
         providerName: deleteProvider.name,
       });
-      toast({ title: t('providers.deleted'), variant: 'success' });
+      toast({ title: t('providers.deleted') });
       setDeleteProvider(null);
       invalidateProviders();
     } catch {
diff --git a/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx b/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
index d2a4789c7a..b28c71adec 100644
--- a/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
+++ b/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
@@ -43,7 +43,6 @@ export function TeamDeleteDialog({
 
       toast({
         title: tSettings('teams.teamDeleted'),
-        variant: 'success',
       });
       onOpenChange(false);
       onSuccess?.();
diff --git a/services/platform/app/features/vendors/components/vendor-import-form.tsx b/services/platform/app/features/vendors/components/vendor-import-form.tsx
index a27ee00ac9..e1d251c392 100644
--- a/services/platform/app/features/vendors/components/vendor-import-form.tsx
+++ b/services/platform/app/features/vendors/components/vendor-import-form.tsx
@@ -128,7 +128,7 @@ export function VendorImportForm({
           </FileUpload.Root>
           <Text as="div" variant="caption" className="leading-relaxed">
             <ul className="list-outside list-disc space-y-2 pl-4">
-              <li>{t('importForm.formatHint')}</li>
+              <li>{t('importForm.expectedColumns')}</li>
               <li>{t('importForm.localeHint')}</li>
             </ul>
           </Text>
diff --git a/services/platform/app/features/vendors/components/vendors-table.tsx b/services/platform/app/features/vendors/components/vendors-table.tsx
index 76b671cd1b..f49cf10249 100644
--- a/services/platform/app/features/vendors/components/vendors-table.tsx
+++ b/services/platform/app/features/vendors/components/vendors-table.tsx
@@ -1,15 +1,17 @@
 'use client';
 
 import { useNavigate } from '@tanstack/react-router';
-import type { Row } from '@tanstack/react-table';
+import type { Row, RowSelectionState } from '@tanstack/react-table';
 import { Store } from 'lucide-react';
 import { useCallback, useMemo, useState } from 'react';
 
 import { DataTable } from '@/app/components/ui/data-table/data-table';
+import { BulkDeleteBar } from '@/app/components/ui/data-table/data-table-bulk-actions';
 import { useListPage } from '@/app/hooks/use-list-page';
 import type { Doc } from '@/convex/_generated/dataModel';
 import { useT } from '@/lib/i18n/client';
 
+import { useDeleteVendor } from '../hooks/mutations';
 import {
   useApproxVendorCount,
   useListVendorsPaginated,
@@ -126,11 +128,25 @@ export function VendorsTable({
   );
 
   const [viewingVendor, setViewingVendor] = useState<Vendor | null>(null);
+  const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
+  const deleteVendor = useDeleteVendor();
 
   const handleRowClick = useCallback((row: Row<Vendor>) => {
     setViewingVendor(row.original);
   }, []);
 
+  const handleClearSelection = useCallback(() => {
+    setRowSelection({});
+  }, []);
+
+  const handleDeleteItem = useCallback(
+    async (id: string) => {
+      const vendorId = id as Doc<'vendors'>['_id'];
+      await deleteVendor.mutateAsync({ vendorId });
+    },
+    [deleteVendor],
+  );
+
   const list = useListPage<Vendor>({
     dataSource: {
       type: 'paginated',
@@ -159,12 +175,23 @@ export function VendorsTable({
         columns={columns}
         stickyLayout={stickyLayout}
         onRowClick={handleRowClick}
+        enableRowSelection
+        rowSelection={rowSelection}
+        onRowSelectionChange={setRowSelection}
         actionMenu={<VendorsActionMenu organizationId={organizationId} />}
         emptyState={{
           icon: Store,
           title: tEmpty('vendors.title'),
           description: tEmpty('vendors.description'),
         }}
+        footer={
+          <BulkDeleteBar
+            rowSelection={rowSelection}
+            onClearSelection={handleClearSelection}
+            onDeleteItem={handleDeleteItem}
+            onDeleteComplete={handleClearSelection}
+          />
+        }
         {...list.tableProps}
       />
 
diff --git a/services/platform/app/features/vendors/hooks/use-vendors-table-config.tsx b/services/platform/app/features/vendors/hooks/use-vendors-table-config.tsx
index a6538034cb..bad67c161c 100644
--- a/services/platform/app/features/vendors/hooks/use-vendors-table-config.tsx
+++ b/services/platform/app/features/vendors/hooks/use-vendors-table-config.tsx
@@ -11,6 +11,7 @@ export const useVendorsTableConfig = createTableConfigHook<'vendors'>(
     defaultSort: '_creationTime',
   },
   ({ tTables, builders }) => [
+    builders.createSelectColumn(),
     {
       accessorKey: 'name',
       header: tTables('headers.name'),
diff --git a/services/platform/app/hooks/__tests__/use-file-import.test.ts b/services/platform/app/hooks/__tests__/use-file-import.test.ts
index 7bcb0b49f4..fc855c9881 100644
--- a/services/platform/app/hooks/__tests__/use-file-import.test.ts
+++ b/services/platform/app/hooks/__tests__/use-file-import.test.ts
@@ -155,9 +155,9 @@ describe('customerMappers.excel', () => {
   });
 });
 
-describe('productMappers.excel', () => {
+describe('productMappers.record', () => {
   it('parses record with lowercase keys', () => {
-    const result = productMappers.excel({
+    const result = productMappers.record({
       name: 'Widget',
       description: 'A fine widget',
       price: 9.99,
@@ -178,17 +178,17 @@ describe('productMappers.excel', () => {
   });
 
   it('falls back to title when name is missing', () => {
-    const result = productMappers.excel({ title: 'Gadget', price: 5 });
+    const result = productMappers.record({ title: 'Gadget', price: 5 });
     expect(result).toMatchObject({ name: 'Gadget' });
   });
 
   it('returns null when name and title are missing', () => {
-    const result = productMappers.excel({ description: 'orphan' });
+    const result = productMappers.record({ description: 'orphan' });
     expect(result).toBeNull();
   });
 
   it('defaults stock to 0, price to 0, currency to USD', () => {
-    const result = productMappers.excel({ name: 'Minimal' });
+    const result = productMappers.record({ name: 'Minimal' });
     expect(result).toMatchObject({
       stock: 0,
       price: 0,
@@ -197,7 +197,7 @@ describe('productMappers.excel', () => {
   });
 
   it('resolves imageurl key (normalized from ImageUrl/imageUrl)', () => {
-    const result = productMappers.excel({
+    const result = productMappers.record({
       name: 'Pic',
       imageurl: 'https://example.com/pic.png',
     });
diff --git a/services/platform/app/hooks/use-file-import.ts b/services/platform/app/hooks/use-file-import.ts
index 897326a1d5..04786e2020 100644
--- a/services/platform/app/hooks/use-file-import.ts
+++ b/services/platform/app/hooks/use-file-import.ts
@@ -227,22 +227,27 @@ export const productMappers = {
     const lowerValue = value.toLowerCase();
     return validStatuses.find((s) => s === lowerValue) ?? defaultStatus;
   },
-  csv: (row: string[], _index: number) => {
-    const name = row[0]?.trim();
-    if (!name) return null;
-
-    return {
-      name,
-      description: row[1]?.trim() || undefined,
-      imageUrl: row[2]?.trim() || undefined,
-      stock: row[3] ? parseInt(row[3], 10) : 0,
-      price: row[4] ? parseFloat(row[4]) : 0,
-      currency: row[5]?.trim() || 'USD',
-      category: row[6]?.trim() || undefined,
-      source: 'manual_import' as const,
-    };
+  /** Expected header names for product imports */
+  expectedHeaders: [
+    'name',
+    'description',
+    'imageurl',
+    'stock',
+    'price',
+    'currency',
+    'category',
+    'status',
+  ] as const,
+  /**
+   * CSV fallback mapper — only runs when headers are NOT detected.
+   * Returns null for every row so the import fails with a clear error
+   * instead of silently misaligning columns by position.
+   */
+  csv: (_row: string[], _index: number) => {
+    return null;
   },
-  excel: (record: Record<string, unknown>) => {
+  /** Record-based mapper used by both CSV (with headers) and Excel imports */
+  record: (record: Record<string, unknown>) => {
     const name = getString(record.name) || getString(record.title);
     if (!name) return null;
 
diff --git a/services/platform/lib/utils/file-parsing.ts b/services/platform/lib/utils/file-parsing.ts
index 3483ad902e..56a703c820 100644
--- a/services/platform/lib/utils/file-parsing.ts
+++ b/services/platform/lib/utils/file-parsing.ts
@@ -13,57 +13,10 @@ export type FileParseResult<T> = {
 type CSVParseOptions = {
   /** Column delimiter (default: comma) */
   delimiter?: string;
-  /** Whether the first row contains headers (default: false) */
-  hasHeaders?: boolean;
   /** Skip empty lines (default: true) */
   skipEmptyLines?: boolean;
 };
 
-const COMMON_HEADER_NAMES = new Set([
-  'name',
-  'email',
-  'description',
-  'price',
-  'sku',
-  'currency',
-  'category',
-  'stock',
-  'status',
-  'locale',
-  'source',
-  'id',
-  'title',
-  'brand',
-  'type',
-  'phone',
-  'address',
-  'country',
-  'city',
-  'image',
-  'imageurl',
-  'image_url',
-  'first name',
-  'last name',
-  'firstname',
-  'lastname',
-  'customer id',
-  'product',
-  'quantity',
-  'amount',
-  'unit',
-  'vendor',
-  'supplier',
-]);
-
-function isHeaderRow(values: string[]): boolean {
-  const nonEmpty = values.filter((v) => v.length > 0);
-  if (nonEmpty.length === 0) return false;
-  const matches = nonEmpty.filter((v) =>
-    COMMON_HEADER_NAMES.has(v.toLowerCase()),
-  );
-  return matches.length >= Math.ceil(nonEmpty.length / 2);
-}
-
 type CSVParseOutput = {
   headers: string[] | null;
   rows: string[][];
@@ -125,7 +78,7 @@ function parseCSVText(
   csvText: string,
   options: CSVParseOptions = {},
 ): CSVParseOutput {
-  const { delimiter = ',', skipEmptyLines = true, hasHeaders } = options;
+  const { delimiter = ',', skipEmptyLines = true } = options;
 
   const lines = csvText.trim().split('\n');
   const rows: string[][] = [];
@@ -139,7 +92,7 @@ function parseCSVText(
   }
 
   let headers: string[] | null = null;
-  if (hasHeaders !== false && rows.length >= 2 && isHeaderRow(rows[0])) {
+  if (rows.length >= 2) {
     headers = rows.shift()?.map((h) => h.toLowerCase()) ?? null;
   }
 
diff --git a/services/platform/messages/de.json b/services/platform/messages/de.json
index 6ffff89669..d4f7b44e62 100644
--- a/services/platform/messages/de.json
+++ b/services/platform/messages/de.json
@@ -3172,7 +3172,8 @@
       "syncBusinessData": "Geschäftsdaten synchronisieren",
       "formatHint": "Format: E-Mail, Name (optional), Sprache (optional). Ein Kunde pro Zeile.",
       "localeHint": "Unterstützte Sprachen: en, fr, de, zh, sowie erweiterte Formate wie en-US, de-DE und de-CH.",
-      "statusNote": "Importierte Kunden werden als (Aktiv) markiert"
+      "statusNote": "Importierte Kunden werden als (Aktiv) markiert",
+      "expectedColumns": "Erwartete Spalten: email, name, locale"
     },
     "editCustomer": "Kunde bearbeiten",
     "deleteCustomer": "Kunde löschen",
@@ -3230,8 +3231,8 @@
     "importForm": {
       "manualEntry": "Manuelle Eingabe",
       "upload": "Upload",
-      "formatHint": "Format: E-Mail, Name (optional), Sprache (optional). Ein Lieferant pro Zeile.",
-      "localeHint": "Du kannst die folgenden unterstützten Sprachen verwenden: en, fr, de, zh, sowie erweiterte Formate wie en-US, de-DE und de-CH."
+      "localeHint": "Du kannst die folgenden unterstützten Sprachen verwenden: en, fr, de, zh, sowie erweiterte Formate wie en-US, de-DE und de-CH.",
+      "expectedColumns": "Erwartete Spalten: email, name, locale"
     },
     "editVendor": "Lieferant bearbeiten",
     "deleteVendor": "Lieferant löschen",
diff --git a/services/platform/messages/en.json b/services/platform/messages/en.json
index f8d9b28daf..a18d2df8cf 100644
--- a/services/platform/messages/en.json
+++ b/services/platform/messages/en.json
@@ -3231,7 +3231,8 @@
       "syncBusinessData": "Sync business data",
       "formatHint": "Format: email, name (optional), locale (optional). One customer per line.",
       "localeHint": "Supported locales: en, fr, de, zh, as well as extended formats like en-US, de-DE, and de-CH.",
-      "statusNote": "Imported customers are tagged as (Active)"
+      "statusNote": "Imported customers are tagged as (Active)",
+      "expectedColumns": "Expected columns: email, name, locale"
     },
     "editCustomer": "Edit customer",
     "deleteCustomer": "Delete customer",
@@ -3289,8 +3290,8 @@
     "importForm": {
       "manualEntry": "Manual entry",
       "upload": "Upload",
-      "formatHint": "Format: email, name (optional), locale (optional). One vendor per line.",
-      "localeHint": "You can use the following supported locales: en, fr, de, zh, as well as extended formats like en-US, de-DE, and de-CH."
+      "localeHint": "You can use the following supported locales: en, fr, de, zh, as well as extended formats like en-US, de-DE, and de-CH.",
+      "expectedColumns": "Expected columns: email, name, locale"
     },
     "editVendor": "Edit vendor",
     "deleteVendor": "Delete vendor",

From 8191bf277d1801891ef077724092979da9129838 Mon Sep 17 00:00:00 2001
From: israel <israeliyanda5@gmail.com>
Date: Fri, 10 Apr 2026 12:43:00 +0100
Subject: [PATCH 2/5] fix(platform): add oxlint disable comment for Convex Id
 type assertion

---
 .../platform/app/features/vendors/components/vendors-table.tsx   | 1 +
 1 file changed, 1 insertion(+)

diff --git a/services/platform/app/features/vendors/components/vendors-table.tsx b/services/platform/app/features/vendors/components/vendors-table.tsx
index f49cf10249..6bd578de43 100644
--- a/services/platform/app/features/vendors/components/vendors-table.tsx
+++ b/services/platform/app/features/vendors/components/vendors-table.tsx
@@ -141,6 +141,7 @@ export function VendorsTable({
 
   const handleDeleteItem = useCallback(
     async (id: string) => {
+      // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Convex Id type from row selection key
       const vendorId = id as Doc<'vendors'>['_id'];
       await deleteVendor.mutateAsync({ vendorId });
     },

From 1686826aa5618a886f3b0ce419312ec50328081b Mon Sep 17 00:00:00 2001
From: israel <israeliyanda5@gmail.com>
Date: Fri, 10 Apr 2026 12:53:10 +0100
Subject: [PATCH 3/5] fix(platform): handle single-row CSV as header-only and
 update locale hint text

- Change CSV header detection from rows.length >= 2 to rows.length > 0
  so a header-only CSV is correctly treated as headers with zero data rows
- Update localeHint text to reflect that locale is no longer restricted
  to a fixed set (now accepts any valid BCP 47 tag)
---
 services/platform/lib/utils/file-parsing.ts | 2 +-
 services/platform/messages/de.json          | 4 ++--
 services/platform/messages/en.json          | 4 ++--
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/services/platform/lib/utils/file-parsing.ts b/services/platform/lib/utils/file-parsing.ts
index 56a703c820..141204c07a 100644
--- a/services/platform/lib/utils/file-parsing.ts
+++ b/services/platform/lib/utils/file-parsing.ts
@@ -92,7 +92,7 @@ function parseCSVText(
   }
 
   let headers: string[] | null = null;
-  if (rows.length >= 2) {
+  if (rows.length > 0) {
     headers = rows.shift()?.map((h) => h.toLowerCase()) ?? null;
   }
 
diff --git a/services/platform/messages/de.json b/services/platform/messages/de.json
index d4f7b44e62..e3c5a53101 100644
--- a/services/platform/messages/de.json
+++ b/services/platform/messages/de.json
@@ -3171,7 +3171,7 @@
       "fromShopify": "Von Shopify",
       "syncBusinessData": "Geschäftsdaten synchronisieren",
       "formatHint": "Format: E-Mail, Name (optional), Sprache (optional). Ein Kunde pro Zeile.",
-      "localeHint": "Unterstützte Sprachen: en, fr, de, zh, sowie erweiterte Formate wie en-US, de-DE und de-CH.",
+      "localeHint": "Sprache kann ein beliebiger BCP 47-Tag sein (z. B. en, en-US, de-DE, zh-Hans).",
       "statusNote": "Importierte Kunden werden als (Aktiv) markiert",
       "expectedColumns": "Erwartete Spalten: email, name, locale"
     },
@@ -3231,7 +3231,7 @@
     "importForm": {
       "manualEntry": "Manuelle Eingabe",
       "upload": "Upload",
-      "localeHint": "Du kannst die folgenden unterstützten Sprachen verwenden: en, fr, de, zh, sowie erweiterte Formate wie en-US, de-DE und de-CH.",
+      "localeHint": "Sprache kann ein beliebiger BCP 47-Tag sein (z. B. en, en-US, de-DE, zh-Hans).",
       "expectedColumns": "Erwartete Spalten: email, name, locale"
     },
     "editVendor": "Lieferant bearbeiten",
diff --git a/services/platform/messages/en.json b/services/platform/messages/en.json
index a18d2df8cf..a175ed64be 100644
--- a/services/platform/messages/en.json
+++ b/services/platform/messages/en.json
@@ -3230,7 +3230,7 @@
       "fromShopify": "From Shopify",
       "syncBusinessData": "Sync business data",
       "formatHint": "Format: email, name (optional), locale (optional). One customer per line.",
-      "localeHint": "Supported locales: en, fr, de, zh, as well as extended formats like en-US, de-DE, and de-CH.",
+      "localeHint": "Locale can be any valid BCP 47 tag (e.g. en, en-US, de-DE, zh-Hans).",
       "statusNote": "Imported customers are tagged as (Active)",
       "expectedColumns": "Expected columns: email, name, locale"
     },
@@ -3290,7 +3290,7 @@
     "importForm": {
       "manualEntry": "Manual entry",
       "upload": "Upload",
-      "localeHint": "You can use the following supported locales: en, fr, de, zh, as well as extended formats like en-US, de-DE, and de-CH.",
+      "localeHint": "Locale can be any valid BCP 47 tag (e.g. en, en-US, de-DE, zh-Hans).",
       "expectedColumns": "Expected columns: email, name, locale"
     },
     "editVendor": "Edit vendor",

From 922b1b8ab08972a6babcf43ab571a2ace8963858 Mon Sep 17 00:00:00 2001
From: israel <israeliyanda5@gmail.com>
Date: Fri, 10 Apr 2026 13:40:57 +0100
Subject: [PATCH 4/5] fix(platform): restore variant success on toast calls to
 match codebase convention

---
 .../platform/app/components/ui/entity/entity-delete-dialog.tsx  | 1 +
 .../app/features/agents/components/agent-delete-dialog.tsx      | 1 +
 .../settings/api-keys/components/api-key-revoke-dialog.tsx      | 1 +
 .../features/settings/providers/components/providers-table.tsx  | 2 +-
 .../features/settings/teams/components/team-delete-dialog.tsx   | 1 +
 5 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/services/platform/app/components/ui/entity/entity-delete-dialog.tsx b/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
index d96bbfc239..42dfd62b1e 100644
--- a/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
+++ b/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
@@ -80,6 +80,7 @@ export function EntityDeleteDialog<TEntity>({
       await deleteMutation(entity);
       toast({
         title: translations.successMessage,
+        variant: 'success',
       });
       onClose();
       onSuccess?.();
diff --git a/services/platform/app/features/agents/components/agent-delete-dialog.tsx b/services/platform/app/features/agents/components/agent-delete-dialog.tsx
index b0dd337342..2cc85a66ea 100644
--- a/services/platform/app/features/agents/components/agent-delete-dialog.tsx
+++ b/services/platform/app/features/agents/components/agent-delete-dialog.tsx
@@ -38,6 +38,7 @@ export function AgentDeleteDialog({
       });
       toast({
         title: t('agents.agentDeleted'),
+        variant: 'success',
       });
       onOpenChange(false);
       onDeleted?.();
diff --git a/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx b/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
index 5390bd14a4..fcea4c55fc 100644
--- a/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
+++ b/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
@@ -35,6 +35,7 @@ export function ApiKeyRevokeDialog({
       onSuccess: () => {
         toast({
           title: tSettings('apiKeys.keyRevoked'),
+          variant: 'success',
         });
         onOpenChange(false);
         onSuccess?.();
diff --git a/services/platform/app/features/settings/providers/components/providers-table.tsx b/services/platform/app/features/settings/providers/components/providers-table.tsx
index 2e2edb2677..bfc65ca1fd 100644
--- a/services/platform/app/features/settings/providers/components/providers-table.tsx
+++ b/services/platform/app/features/settings/providers/components/providers-table.tsx
@@ -90,7 +90,7 @@ export function ProvidersTable({ organizationId }: ProvidersTableProps) {
         orgSlug: 'default',
         providerName: deleteProvider.name,
       });
-      toast({ title: t('providers.deleted') });
+      toast({ title: t('providers.deleted'), variant: 'success' });
       setDeleteProvider(null);
       invalidateProviders();
     } catch {
diff --git a/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx b/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
index b28c71adec..d2a4789c7a 100644
--- a/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
+++ b/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
@@ -43,6 +43,7 @@ export function TeamDeleteDialog({
 
       toast({
         title: tSettings('teams.teamDeleted'),
+        variant: 'success',
       });
       onOpenChange(false);
       onSuccess?.();

From 23f77d70c4086a40c2870a558872969d3d3a4e58 Mon Sep 17 00:00:00 2001
From: israel <israeliyanda5@gmail.com>
Date: Fri, 10 Apr 2026 14:05:49 +0100
Subject: [PATCH 5/5] fix(platform): use default (info) toast variant for
 delete/revoke actions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Delete and revoke confirmations are neutral acknowledgements, not
celebratory successes — show the info icon instead of the green check.
---
 .../platform/app/components/ui/entity/entity-delete-dialog.tsx  | 1 -
 .../app/features/agents/components/agent-delete-dialog.tsx      | 1 -
 .../settings/api-keys/components/api-key-revoke-dialog.tsx      | 1 -
 .../features/settings/providers/components/providers-table.tsx  | 2 +-
 .../features/settings/teams/components/team-delete-dialog.tsx   | 1 -
 5 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/services/platform/app/components/ui/entity/entity-delete-dialog.tsx b/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
index 42dfd62b1e..d96bbfc239 100644
--- a/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
+++ b/services/platform/app/components/ui/entity/entity-delete-dialog.tsx
@@ -80,7 +80,6 @@ export function EntityDeleteDialog<TEntity>({
       await deleteMutation(entity);
       toast({
         title: translations.successMessage,
-        variant: 'success',
       });
       onClose();
       onSuccess?.();
diff --git a/services/platform/app/features/agents/components/agent-delete-dialog.tsx b/services/platform/app/features/agents/components/agent-delete-dialog.tsx
index 2cc85a66ea..b0dd337342 100644
--- a/services/platform/app/features/agents/components/agent-delete-dialog.tsx
+++ b/services/platform/app/features/agents/components/agent-delete-dialog.tsx
@@ -38,7 +38,6 @@ export function AgentDeleteDialog({
       });
       toast({
         title: t('agents.agentDeleted'),
-        variant: 'success',
       });
       onOpenChange(false);
       onDeleted?.();
diff --git a/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx b/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
index fcea4c55fc..5390bd14a4 100644
--- a/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
+++ b/services/platform/app/features/settings/api-keys/components/api-key-revoke-dialog.tsx
@@ -35,7 +35,6 @@ export function ApiKeyRevokeDialog({
       onSuccess: () => {
         toast({
           title: tSettings('apiKeys.keyRevoked'),
-          variant: 'success',
         });
         onOpenChange(false);
         onSuccess?.();
diff --git a/services/platform/app/features/settings/providers/components/providers-table.tsx b/services/platform/app/features/settings/providers/components/providers-table.tsx
index bfc65ca1fd..2e2edb2677 100644
--- a/services/platform/app/features/settings/providers/components/providers-table.tsx
+++ b/services/platform/app/features/settings/providers/components/providers-table.tsx
@@ -90,7 +90,7 @@ export function ProvidersTable({ organizationId }: ProvidersTableProps) {
         orgSlug: 'default',
         providerName: deleteProvider.name,
       });
-      toast({ title: t('providers.deleted'), variant: 'success' });
+      toast({ title: t('providers.deleted') });
       setDeleteProvider(null);
       invalidateProviders();
     } catch {
diff --git a/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx b/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
index d2a4789c7a..b28c71adec 100644
--- a/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
+++ b/services/platform/app/features/settings/teams/components/team-delete-dialog.tsx
@@ -43,7 +43,6 @@ export function TeamDeleteDialog({
 
       toast({
         title: tSettings('teams.teamDeleted'),
-        variant: 'success',
       });
       onOpenChange(false);
       onSuccess?.();
