From 1b97371f73ea62dbe549d34363e0b38006b60e66 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Wed, 29 Apr 2026 18:51:18 +0530 Subject: [PATCH] feat: enhance error handling across UI components and services to display server-provided error messages and update SMTP configuration deletion logic --- src/components/layout/Sidebar.tsx | 69 +-- src/components/shared/FailedEmailsTable.tsx | 8 +- .../shared/MultiselectPaginatedSelect.tsx | 2 +- src/components/shared/SupplierModal.tsx | 4 +- src/components/shared/SuppliersTable.tsx | 42 +- src/components/shared/ViewSupplierModal.tsx | 2 +- src/pages/superadmin/SmtpConfig.tsx | 2 +- src/pages/tenant/CreateDocument.tsx | 12 +- src/pages/tenant/DocumentCategories.tsx | 550 +++++++++++------- src/pages/tenant/EditDocument.tsx | 26 +- src/pages/tenant/LandingPage.tsx | 144 ++++- src/pages/tenant/NotificationSettings.tsx | 14 +- src/pages/tenant/NotificationTemplates.tsx | 7 +- src/pages/tenant/StorageDashboard.tsx | 304 +++++++--- src/pages/tenant/ViewDocument.tsx | 6 + src/services/smtp-config-service.ts | 5 +- src/utils/toast.ts | 8 +- 17 files changed, 814 insertions(+), 391 deletions(-) diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index da376fe..770dfb3 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -117,34 +117,6 @@ const tenantAdminPlatformMenu: MenuItem[] = [ ]; const tenantAdminPlatformServiceMenu: MenuItem[] = [ - { - icon: Bot, - label: "AI Services", - isGroup: true, - children: [ - { - label: "Completion History", - path: "/tenant/ai/completions", - requiredPermission: { resource: "ai" }, - }, - { - label: "Prompt Management", - path: "/tenant/ai/prompts", - requiredPermission: { resource: "ai" }, - }, - { - label: "Tenant Config", - path: "/tenant/ai/config", - requiredPermission: { resource: "ai" }, - }, - { - label: "Knowledge (RAG)", - path: "/tenant/ai/knowledge", - requiredPermission: { resource: "ai" }, - }, - ], - requiredPermission: { resource: "ai" }, - }, { icon: Paperclip, label: "File Attachments", @@ -209,6 +181,34 @@ const tenantAdminPlatformServiceMenu: MenuItem[] = [ ], requiredPermission: { resource: "document" }, }, + { + icon: Bot, + label: "AI Services", + isGroup: true, + children: [ + { + label: "Completion History", + path: "/tenant/ai/completions", + requiredPermission: { resource: "ai" }, + }, + { + label: "Prompt Management", + path: "/tenant/ai/prompts", + requiredPermission: { resource: "ai" }, + }, + { + label: "Tenant Config", + path: "/tenant/ai/config", + requiredPermission: { resource: "ai" }, + }, + { + label: "Knowledge (RAG)", + path: "/tenant/ai/knowledge", + requiredPermission: { resource: "ai" }, + }, + ], + requiredPermission: { resource: "ai" }, + }, { icon: Package, label: "Modules", path: "/tenant/modules" }, ]; @@ -248,7 +248,7 @@ const tenantAdminSystemMenu: MenuItem[] = [ { label: "Failed Emails", path: "/tenant/settings/failed-emails", - } + }, ], requiredPermission: { resource: "tenants" }, }, @@ -624,7 +624,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{roleName} @@ -685,7 +685,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { alt="Logo" className="h-9 w-auto max-w-[180px] object-contain" fallback={ -
@@ -711,7 +711,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{roleName} @@ -728,7 +728,10 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { )} {platformServiceMenu.length > 0 && ( - + )} {/* System Menu */} diff --git a/src/components/shared/FailedEmailsTable.tsx b/src/components/shared/FailedEmailsTable.tsx index 4c696b8..f3531b6 100644 --- a/src/components/shared/FailedEmailsTable.tsx +++ b/src/components/shared/FailedEmailsTable.tsx @@ -15,6 +15,7 @@ export const FailedEmailsTable: React.FC = () => { const [total, setTotal] = useState(0); const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(50); + const [error, setError] = useState(null); const [selectedEmail, setSelectedEmail] = useState(null); const [isModalVisible, setIsModalVisible] = useState(false); @@ -30,9 +31,9 @@ export const FailedEmailsTable: React.FC = () => { setTotal(res.total || 0); setCurrentPage(page); } catch (error: any) { - toast.error('Error fetching failed emails', { - description: error.message - }); + setError( + error?.response?.data?.error?.message || "Failed to load failed emails", + ); } finally { setLoading(false); } @@ -181,6 +182,7 @@ export const FailedEmailsTable: React.FC = () => { keyExtractor={(item) => item.id} isLoading={loading} emptyMessage="No failed emails found" + error={error} /> {total > limit && ( diff --git a/src/components/shared/MultiselectPaginatedSelect.tsx b/src/components/shared/MultiselectPaginatedSelect.tsx index f4062b0..12d1658 100644 --- a/src/components/shared/MultiselectPaginatedSelect.tsx +++ b/src/components/shared/MultiselectPaginatedSelect.tsx @@ -249,7 +249,7 @@ export const MultiselectPaginatedSelect = ({ className="flex items-center gap-1.5 text-[13px] font-medium text-[#0e1b2a]" > {label} - {required && *} + {required && *}
- - {isSubmitting ? "Processing..." : editingCategory ? "Update Category" : "Create Category"} - -
+
+ + + {isSubmitting + ? "Processing..." + : editingCategory + ? "Update Category" + : "Create Category"} + +
{/* View Modal */} { setIsViewModalOpen(false); setViewingCategory(null); }} + onClose={() => { + setIsViewModalOpen(false); + setViewingCategory(null); + }} title="Document Category Details" maxWidth="lg" >
-
-
- -

{viewingCategory?.name}

-
-
- -

- {viewingCategory?.code} -

-
-
- -

{viewingCategory?.review_frequency_months} months

-
-
- -

{viewingCategory?.retention_years} years

-
-
- -

- {categories.find(c => c.id === viewingCategory?.parent_id)?.name || "None (Root Category)"} -

-
+
+
+ +

+ {viewingCategory?.name} +

- -

{viewingCategory?.description || "No description provided."}

+ +

+ + {viewingCategory?.code} + +

-
-
-

Requires Training

-

Training acknowledgement is {viewingCategory?.requires_training ? "enabled" : "disabled"} for this category.

-
-
-
-
+
+ +

+ {viewingCategory?.review_frequency_months} months +

-
- +
+ +

+ {viewingCategory?.retention_years} years +

+
+ +

+ {categories.find((c) => c.id === viewingCategory?.parent_id) + ?.name || "None (Root Category)"} +

+
+
+
+ +

+ {viewingCategory?.description || "No description provided."} +

+
+
+
+

+ Requires Training +

+

+ Training acknowledgement is{" "} + {viewingCategory?.requires_training ? "enabled" : "disabled"}{" "} + for this category. +

+
+
+
+
+
+
+ +
diff --git a/src/pages/tenant/EditDocument.tsx b/src/pages/tenant/EditDocument.tsx index f814f93..6be1567 100644 --- a/src/pages/tenant/EditDocument.tsx +++ b/src/pages/tenant/EditDocument.tsx @@ -4,7 +4,12 @@ import { useForm, Controller } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Layout } from "@/components/layout/Layout"; -import { FormField, FormSelect, FormTextArea, PrimaryButton } from "@/components/shared"; +import { + FormField, + FormSelect, + FormTextArea, + PrimaryButton, +} from "@/components/shared"; import { documentService } from "@/services/document-service"; import type { DocumentCategory } from "@/types/document"; import { showToast } from "@/utils/toast"; @@ -59,28 +64,31 @@ const EditDocument = (): ReactElement => { moduleService.getMyModules(), documentService.getById(id), ]); - + setCategories(categoriesRes.data || []); const myModules = modulesRes.data || []; setModules(myModules); const doc = docRes.data; // Find matching module by id (UUID) or module_id (code) - const matchedModule = myModules.find(m => - (doc.source_module_id && m.id === doc.source_module_id) || - (doc.source_module && m.module_id === doc.source_module) + const matchedModule = myModules.find( + (m) => + (doc.source_module_id && m.id === doc.source_module_id) || + (doc.source_module && m.module_id === doc.source_module), ); reset({ title: doc.title, description: doc.description || "", - category_id: doc.category_id || "", + category_id: doc.category_id || doc.category?.id || "", department: doc.department || "", tags: (doc.tags || []).join(", "), selectedModuleId: matchedModule?.id || "", }); } catch (err: any) { - showToast.error(err?.response?.data?.error?.message || "Failed to load document data"); + showToast.error( + err?.response?.data?.error?.message || "Failed to load document data", + ); navigate("/tenant/documents"); } finally { setIsLoading(false); @@ -104,7 +112,8 @@ const EditDocument = (): ReactElement => { .map((tag) => tag.trim()) .filter(Boolean) : [], - source_module: modules.find((m) => m.id === data.selectedModuleId)!.module_id, + source_module: modules.find((m) => m.id === data.selectedModuleId)! + .module_id, source_module_id: data.selectedModuleId, }); showToast.success("Document updated successfully"); @@ -239,7 +248,6 @@ const EditDocument = (): ReactElement => {
-
-
-

{user?.first_name ? `${user.first_name} ${user.last_name || ''}` : getUserName()}

-

{user?.email}

-
-
- - {user?.first_name ? user.first_name[0].toUpperCase() : 'U'} - +
+ + + {/* Dropdown Menu */} + {isDropdownOpen && ( +
e.stopPropagation()} + > + {/* User Info Section */} +
+
+
+ +
+
+

+ {getUserDisplayName()} +

+

{user?.email}

+
+
+
+ + {/* Logout Button */} +
+ +
+
+ )}
diff --git a/src/pages/tenant/NotificationSettings.tsx b/src/pages/tenant/NotificationSettings.tsx index 5dc9b0b..64f47ab 100644 --- a/src/pages/tenant/NotificationSettings.tsx +++ b/src/pages/tenant/NotificationSettings.tsx @@ -90,9 +90,17 @@ export const NotificationSettings = () => { if (isLoading || !preferences) { return ( -
- Loading preferences... -
+ +
+ Loading preferences... +
+
); } diff --git a/src/pages/tenant/NotificationTemplates.tsx b/src/pages/tenant/NotificationTemplates.tsx index d39349c..13baf5a 100644 --- a/src/pages/tenant/NotificationTemplates.tsx +++ b/src/pages/tenant/NotificationTemplates.tsx @@ -85,6 +85,7 @@ const NotificationTemplates = (): ReactElement => { const [modules, setModules] = useState([]); const [selectedModule, setSelectedModule] = useState("all"); const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); // Pagination const [currentPage, setCurrentPage] = useState(1); @@ -138,7 +139,9 @@ const NotificationTemplates = (): ReactElement => { setTotalPages(res.pagination?.pages || 1); } } catch (err: any) { - showToast.error("Failed to load templates"); + setError( + err?.response?.data?.error?.message || "Failed to load templates", + ); } finally { setIsLoading(false); } @@ -329,6 +332,8 @@ const NotificationTemplates = (): ReactElement => { data={templates} isLoading={isLoading} keyExtractor={(t) => t.code} + error={error} + emptyMessage="No templates found" />
diff --git a/src/pages/tenant/StorageDashboard.tsx b/src/pages/tenant/StorageDashboard.tsx index f0d2729..4ae741d 100644 --- a/src/pages/tenant/StorageDashboard.tsx +++ b/src/pages/tenant/StorageDashboard.tsx @@ -4,7 +4,7 @@ import { ShieldCheck, Building2, Package, - AlertCircle, + // AlertCircle, Pencil, HardDrive, Files, @@ -51,12 +51,23 @@ interface QuotaEditModalProps { onUpdated: () => void; } -const QuotaEditModal = ({ isOpen, onClose, quota, onUpdated }: QuotaEditModalProps) => { +const QuotaEditModal = ({ + isOpen, + onClose, + quota, + onUpdated, +}: QuotaEditModalProps) => { const [maxStorageMB, setMaxStorageMB] = useState( - Math.floor((typeof quota.max_storage_bytes === 'string' ? parseInt(quota.max_storage_bytes) : quota.max_storage_bytes) / 1024 / 1024) + Math.floor( + (typeof quota.max_storage_bytes === "string" + ? parseInt(quota.max_storage_bytes) + : quota.max_storage_bytes) / + 1024 / + 1024, + ), ); const [maxFileMB, setMaxFileMB] = useState( - Math.floor(quota.max_file_size_bytes / 1024 / 1024) + Math.floor(quota.max_file_size_bytes / 1024 / 1024), ); const [isUpdating, setIsUpdating] = useState(false); @@ -85,8 +96,16 @@ const QuotaEditModal = ({ isOpen, onClose, quota, onUpdated }: QuotaEditModalPro footer={ <> {/* Cancel */} - - {isUpdating ? : } + + {isUpdating ? ( + + ) : ( + + )} Save Changes @@ -115,7 +134,7 @@ const QuotaEditModal = ({ isOpen, onClose, quota, onUpdated }: QuotaEditModalPro const StorageDashboard = (): ReactElement => { const { primaryColor } = useAppTheme(); - const [activeTab, setActiveTab] = useState<'stats' | 'quota'>('stats'); + const [activeTab, setActiveTab] = useState<"stats" | "quota">("stats"); const [stats, setStats] = useState(null); const [quota, setQuota] = useState(null); const [loading, setLoading] = useState(true); @@ -131,8 +150,11 @@ const StorageDashboard = (): ReactElement => { ]); setStats(statsRes.data); setQuota(quotaRes.data); - } catch (err) { - setError("Failed to load dashboard data"); + } catch (err: any) { + setError( + err?.response?.data?.error?.message || "Failed to load dashboard data", + ); + console.log("Failed to load dashboard data", err); } finally { setLoading(false); } @@ -144,11 +166,15 @@ const StorageDashboard = (): ReactElement => { if (loading) { return ( -
- +
); @@ -156,14 +182,15 @@ const StorageDashboard = (): ReactElement => { if (error || !stats || !quota) { return ( -
- -

Error

-

{error}

- Retry + {/* */} + {/*

Error

*/} +

{error}

+ {/* Retry */}
); @@ -175,74 +202,120 @@ const StorageDashboard = (): ReactElement => { // breadcrumbs={[{ label: "File Attachments" }, { label: "Storage Dashboard" }]} pageHeader={{ title: "Storage Dashboard", - description: "Overview of storage consumption, file counts, and quota limits.", + description: + "Overview of storage consumption, file counts, and quota limits.", }} >
{/* Tabs */}
- {activeTab === 'stats' && ( + {activeTab === "stats" && (
{/* Summary Cards */}
-
- +
+
- Usage + + Usage +
-

{stats.quota.usage_percent}% capacity

+

+ {stats.quota.usage_percent}%{" "} + + capacity + +

-
+
-
- Total Files +
+ +
+ + Total Files +
-

{stats.files.total}

+

+ {stats.files.total} +

-
- Images +
+ +
+ + Images +
-

{stats.files.images}

+

+ {stats.files.images} +

-
- DOCs / PDFs +
+ +
+ + DOCs / PDFs +
-

{stats.files.pdfs + stats.files.documents}

+

+ {stats.files.pdfs + stats.files.documents} +

@@ -251,8 +324,13 @@ const StorageDashboard = (): ReactElement => { {/* Entity Table */}
- -

By Entity Type

+ +

+ By Entity Type +

@@ -264,10 +342,19 @@ const StorageDashboard = (): ReactElement => { {Object.entries(stats.by_entity).map(([name, data]) => ( - - - - + + + + ))} @@ -278,7 +365,9 @@ const StorageDashboard = (): ReactElement => {
-

By Source Module

+

+ By Source Module +

{name}{data.count}{formatBytes(data.size)}
+ {name} + + {data.count} + + {formatBytes(data.size)} +
@@ -290,10 +379,19 @@ const StorageDashboard = (): ReactElement => { {Object.entries(stats.by_module).map(([name, data]) => ( - - - - + + + + ))} @@ -303,13 +401,18 @@ const StorageDashboard = (): ReactElement => { )} - {activeTab === 'quota' && ( + {activeTab === "quota" && (
- -

Quota Profile

+ +

+ Quota Profile +

setIsEditModalOpen(true)} @@ -324,18 +427,48 @@ const StorageDashboard = (): ReactElement => {
{name}{data.count}{formatBytes(data.size)}
+ {name} + + {data.count} + + {formatBytes(data.size)} +
{[ - { label: "Max Total Storage", value: quota.max_storage_formatted || formatBytes(quota.max_storage_bytes), icon: HardDrive }, - { label: "Max Per-File Size", value: quota.max_file_size_formatted || formatBytes(quota.max_file_size_bytes), icon: FileText }, - { label: "Currently Used", value: quota.used_storage_formatted || formatBytes(quota.used_storage_bytes), icon: Save }, - { label: "File Count", value: `${quota.file_count} items`, icon: Files }, - { label: "Last Updated", value: new Date(quota.updated_at).toLocaleString(), icon: CheckCircle2 }, + { + label: "Max Total Storage", + value: + quota.max_storage_formatted || + formatBytes(quota.max_storage_bytes), + icon: HardDrive, + }, + { + label: "Max Per-File Size", + value: + quota.max_file_size_formatted || + formatBytes(quota.max_file_size_bytes), + icon: FileText, + }, + { + label: "Currently Used", + value: + quota.used_storage_formatted || + formatBytes(quota.used_storage_bytes), + icon: Save, + }, + { + label: "File Count", + value: `${quota.file_count} items`, + icon: Files, + }, + { + label: "Last Updated", + value: new Date(quota.updated_at).toLocaleString(), + icon: CheckCircle2, + }, ].map((row) => ( + - ))} @@ -343,21 +476,36 @@ const StorageDashboard = (): ReactElement => { -
- -
-

System Security Policy

-

- The following extensions are strictly blocked to prevent malicious execution: -

-
- {quota.blocked_extensions?.map(ext => ( - - {ext} - - ))} -
-
+
+ +
+

+ System Security Policy +

+

+ The following extensions are strictly blocked to prevent + malicious execution: +

+
+ {quota.blocked_extensions?.map((ext) => ( + + {ext} + + ))} +
+
)} diff --git a/src/pages/tenant/ViewDocument.tsx b/src/pages/tenant/ViewDocument.tsx index 73f87b7..d508cde 100644 --- a/src/pages/tenant/ViewDocument.tsx +++ b/src/pages/tenant/ViewDocument.tsx @@ -466,6 +466,11 @@ const ViewDocument = (): ReactElement => { label: "Change Summary", render: (version) => version.change_summary || "-", }, + { + key: "change_reason", + label: "Change Reason", + render: (version) => version.change_reason || "-", + }, { key: "module_name", label: "Module", @@ -912,6 +917,7 @@ const ViewDocument = (): ReactElement => { className="w-full px-3 py-2 border border-[rgba(0,0,0,0.08)] rounded-lg text-sm focus:ring-1 focus:ring-[#112868]/20 focus:outline-none transition-all" /> +
diff --git a/src/services/smtp-config-service.ts b/src/services/smtp-config-service.ts index 610cdff..f13fea6 100644 --- a/src/services/smtp-config-service.ts +++ b/src/services/smtp-config-service.ts @@ -48,8 +48,9 @@ class SmtpConfigService { return response.data; } - async deleteConfig(id: string) { - const response = await apiClient.delete(`/smtp-config/${id}`); + async deleteConfig(id: string, tenantId?: string | null) { + const params = tenantId ? { tenantId } : {}; + const response = await apiClient.delete(`/smtp-config/${id}`, { params }); return response.data; } } diff --git a/src/utils/toast.ts b/src/utils/toast.ts index b88fd2e..60f0017 100644 --- a/src/utils/toast.ts +++ b/src/utils/toast.ts @@ -16,9 +16,13 @@ export const showToast = { duration: 3000, }); }, - error: (message: string, description?: string, action?: ToastAction) => { + error: ( + message: string, + // description?: string, + action?: ToastAction + ) => { toast.error(message, { - description, + // description, action, duration: 4000, });
- {row.label} + + {row.label} + + + {row.value} {row.value}