diff --git a/src/app/dashboard/agent-types/[id]/sections/page.tsx b/src/app/dashboard/agent-types/[id]/sections/page.tsx new file mode 100644 index 0000000..9803e71 --- /dev/null +++ b/src/app/dashboard/agent-types/[id]/sections/page.tsx @@ -0,0 +1,349 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import { useRouter, useParams } from 'next/navigation'; +import Link from 'next/link'; +import { profileSectionsService, AgentTypeSectionsResponse, SectionOrderItem, getErrorMessage } from '@/services'; + +interface SectionWithOrder { + id: string; + name: string; + slug: string; + description: string | null; + icon: string | null; + sortOrder: number; + isActive: boolean; + isGlobal: boolean; + isSystem: boolean; + isRequired: boolean; + effectiveSortOrder: number; + hasCustomOrder: boolean; + _count?: { + fields: number; + }; +} + +export default function AgentTypeSectionsPage() { + const router = useRouter(); + const params = useParams(); + const agentTypeId = params.id as string; + + const [data, setData] = useState(null); + const [sections, setSections] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(''); + const [hasChanges, setHasChanges] = useState(false); + const [draggedIndex, setDraggedIndex] = useState(null); + + const fetchData = useCallback(async () => { + setIsLoading(true); + setError(''); + try { + const response = await profileSectionsService.getSectionsForAgentType(agentTypeId, false); + setData(response); + setSections(response.sections as SectionWithOrder[]); + setHasChanges(false); + } catch (err) { + const errorMessage = getErrorMessage(err); + setError(errorMessage); + if (errorMessage.includes('Unauthorized')) { + router.push('/login'); + } + } finally { + setIsLoading(false); + } + }, [agentTypeId, router]); + + useEffect(() => { + fetchData(); + }, [fetchData]); + + const handleDragStart = (index: number) => { + setDraggedIndex(index); + }; + + const handleDragOver = (e: React.DragEvent, index: number) => { + e.preventDefault(); + if (draggedIndex === null || draggedIndex === index) return; + + const newSections = [...sections]; + const draggedItem = newSections[draggedIndex]; + newSections.splice(draggedIndex, 1); + newSections.splice(index, 0, draggedItem); + + // Update effective sort orders + newSections.forEach((section, idx) => { + section.effectiveSortOrder = idx; + }); + + setSections(newSections); + setDraggedIndex(index); + setHasChanges(true); + }; + + const handleDragEnd = () => { + setDraggedIndex(null); + }; + + const moveSection = (index: number, direction: 'up' | 'down') => { + const newIndex = direction === 'up' ? index - 1 : index + 1; + if (newIndex < 0 || newIndex >= sections.length) return; + + const newSections = [...sections]; + const temp = newSections[index]; + newSections[index] = newSections[newIndex]; + newSections[newIndex] = temp; + + // Update effective sort orders + newSections.forEach((section, idx) => { + section.effectiveSortOrder = idx; + }); + + setSections(newSections); + setHasChanges(true); + }; + + const toggleRequired = (index: number) => { + const newSections = [...sections]; + newSections[index] = { + ...newSections[index], + isRequired: !newSections[index].isRequired, + }; + setSections(newSections); + setHasChanges(true); + }; + + const handleSave = async () => { + setIsSaving(true); + setError(''); + try { + const sectionOrders: SectionOrderItem[] = sections.map((section, index) => ({ + sectionId: section.id, + sortOrder: index, + isRequired: section.isRequired, + })); + await profileSectionsService.updateSectionOrderForAgentType(agentTypeId, sectionOrders); + setHasChanges(false); + // Refresh to get updated hasCustomOrder flags + await fetchData(); + } catch (err) { + setError(getErrorMessage(err)); + } finally { + setIsSaving(false); + } + }; + + const handleReset = () => { + fetchData(); + }; + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Breadcrumb */} +
+ +
+ + {/* Page Title */} +
+

+ Manage Sections for {data?.agentType.name} +

+

+ Customize the order and requirements of profile sections for this agent type. + Drag to reorder or use the arrow buttons. +

+
+ + {/* Sections List */} +
+
+
+
+ + {sections.length} section{sections.length !== 1 ? 's' : ''} + + {hasChanges && ( + + Unsaved Changes + + )} +
+
+ {hasChanges && ( + <> + + + + )} +
+
+
+ + {error && ( +
+

{error}

+
+ )} + + {sections.length === 0 ? ( +
+ + + +

No sections assigned to this agent type

+ + Go to Profile Sections + +
+ ) : ( +
+ {sections.map((section, index) => ( +
handleDragStart(index)} + onDragOver={(e) => handleDragOver(e, index)} + onDragEnd={handleDragEnd} + className={`px-6 py-4 flex items-center justify-between hover:bg-gray-50 cursor-move transition-colors ${ + draggedIndex === index ? 'bg-blue-50 border-2 border-blue-300 border-dashed' : '' + }`} + > +
+ {/* Drag Handle */} +
+ + + +
+ + {/* Order Number */} +
+ {index + 1} +
+ + {/* Section Info */} +
+
+ {section.name} + {section.isGlobal && ( + + Global + + )} + {section.isSystem && ( + + System + + )} + {section.hasCustomOrder && ( + + Custom Order + + )} +
+
+ {section.description || 'No description'} +
+
+
+ +
+ {/* Required Toggle */} + + + {/* Move Buttons */} +
+ + +
+
+
+ ))} +
+ )} +
+ + {/* Info Box */} +
+

How Section Ordering Works

+
    +
  • Global sections (purple badge) are automatically included for all agent types.
  • +
  • System sections (blue badge) cannot be deleted, only hidden.
  • +
  • Custom Order (yellow badge) indicates this agent type has a custom order for that section.
  • +
  • Drag sections or use the arrow buttons to change the display order.
  • +
  • Mark sections as Required to enforce completion by agents.
  • +
  • Click Save Order to apply your changes.
  • +
+
+
+ ); +} diff --git a/src/app/dashboard/agent-types/page.tsx b/src/app/dashboard/agent-types/page.tsx index 05207e5..ee045b4 100644 --- a/src/app/dashboard/agent-types/page.tsx +++ b/src/app/dashboard/agent-types/page.tsx @@ -271,6 +271,12 @@ export default function AgentTypesPage() {
{agentType._count?.agents || 0}
+ + + + {/* Specific agent type assignments */} + {section.agentTypeSections && section.agentTypeSections.length > 0 ? (
+

+ Specific Assignments +

{section.agentTypeSections.map((ats) => { const agentType = agentTypes.find((at) => at.id === ats.agentTypeId); return ( @@ -518,13 +548,17 @@ export default function SectionDetailPage() {
) : (
-

Not assigned to any agent type

+

+ {section.isGlobal + ? 'No specific agent type configurations yet' + : 'Not assigned to any agent type'} +

{availableAgentTypes.length > 0 && ( )}
diff --git a/src/app/dashboard/profile-sections/page.tsx b/src/app/dashboard/profile-sections/page.tsx index 0848906..c941262 100644 --- a/src/app/dashboard/profile-sections/page.tsx +++ b/src/app/dashboard/profile-sections/page.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; +import SectionIcon from '@/components/SectionIcon'; import { profileSectionsService, ProfileSection, @@ -254,7 +255,9 @@ export default function ProfileSectionsPage() {
{section.icon && ( - {section.icon} + + + )}
{section.name}
@@ -280,7 +283,11 @@ export default function ProfileSectionsPage() {
- {section._count?.agentTypeSections || 0} assigned + {section.isGlobal ? ( + All (Global) + ) : ( + `${section._count?.agentTypeSections || 0} assigned` + )}
diff --git a/src/components/SectionIcon.tsx b/src/components/SectionIcon.tsx new file mode 100644 index 0000000..a477234 --- /dev/null +++ b/src/components/SectionIcon.tsx @@ -0,0 +1,101 @@ +'use client'; + +interface SectionIconProps { + name: string | null | undefined; + className?: string; +} + +// Map icon names to SVG paths +const iconPaths: Record = { + // User/Person icons + 'user': 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z', + 'person': 'M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z', + + // Contact icons + 'phone': 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z', + 'telephone': 'M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z', + + // Location icons + 'map-pin': 'M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z', + 'location': 'M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z', + 'location_on': 'M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z M15 11a3 3 0 11-6 0 3 3 0 016 0z', + + // Building icons + 'building': 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4', + 'office': 'M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4', + + // House icons + 'home': 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6', + 'house': 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6', + + // Work icons + 'briefcase': 'M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z', + 'work': 'M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z', + + // Education icons + 'academic-cap': 'M12 14l9-5-9-5-9 5 9 5z M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z M12 14l9-5-9-5-9 5 9 5zm0 0l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14zm-4 6v-7.5l4-2.222', + 'education': 'M12 14l9-5-9-5-9 5 9 5z M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-2.998 12.078 12.078 0 01.665-6.479L12 14z', + + // Bank icons + 'bank': 'M3 21h18M3 10h18M5 6l7-3 7 3M4 10v11M20 10v11M8 14v3M12 14v3M16 14v3', + 'library': 'M3 21h18M3 10h18M5 6l7-3 7 3M4 10v11M20 10v11M8 14v3M12 14v3M16 14v3', + + // Experience icons + 'star': 'M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z', + 'experience': 'M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z', + + // Document icons + 'document': 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', + 'file': 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', + + // Mail icons + 'mail': 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z', + 'email': 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z', + + // Globe icons + 'globe': 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9', + 'world': 'M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9', + + // Settings/cog icons + 'cog': 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z', + 'settings': 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z M15 12a3 3 0 11-6 0 3 3 0 016 0z', + + // Key icons + 'key': 'M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z', + + // Info icons + 'info': 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', + 'information': 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', + + // Default/fallback + 'default': 'M4 6h16M4 10h16M4 14h16M4 18h16', +}; + +export default function SectionIcon({ name, className = 'w-5 h-5' }: SectionIconProps) { + if (!name) return null; + + const iconName = name.toLowerCase().replace(/\s+/g, '-'); + const path = iconPaths[iconName] || iconPaths['default']; + + // Handle multi-path icons (like map-pin) + const paths = path.split(' M').map((p, i) => i === 0 ? p : 'M' + p); + + return ( + + {paths.map((p, i) => ( + + ))} + + ); +} diff --git a/src/services/index.ts b/src/services/index.ts index 41d4d33..a687f35 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -37,6 +37,8 @@ export type { UpdateSectionDto, AssignSectionDto, UpdateAssignmentDto, + SectionOrderItem, + AgentTypeSectionsResponse, } from './profile-sections.service'; // Profile Fields Service diff --git a/src/services/profile-sections.service.ts b/src/services/profile-sections.service.ts index f19f093..21091f2 100644 --- a/src/services/profile-sections.service.ts +++ b/src/services/profile-sections.service.ts @@ -66,6 +66,26 @@ export interface UpdateAssignmentDto { isRequired?: boolean; } +export interface SectionOrderItem { + sectionId: string; + sortOrder: number; + isRequired?: boolean; +} + +export interface AgentTypeSectionsResponse { + agentType: { + id: string; + name: string; + description: string | null; + }; + sections: (ProfileSection & { + isRequired: boolean; + isGlobal: boolean; + effectiveSortOrder: number; + hasCustomOrder: boolean; + })[]; +} + // Profile Sections Service class ProfileSectionsService { private basePath = '/profile-sections'; @@ -121,6 +141,26 @@ class ProfileSectionsService { async removeFromAgentType(sectionId: string, agentTypeId: string): Promise { await api.delete(`${this.basePath}/${sectionId}/assignment/${agentTypeId}`); } + + // Agent Type Sections methods + async getSectionsForAgentType(agentTypeId: string, includeFields = true): Promise { + const url = includeFields + ? `${this.basePath}/agent-type/${agentTypeId}` + : `${this.basePath}/agent-type/${agentTypeId}?includeFields=false`; + const response = await api.get>(url); + return response.data.data; + } + + async updateSectionOrderForAgentType( + agentTypeId: string, + sectionOrders: SectionOrderItem[] + ): Promise<{ success: boolean; updated: number }> { + const response = await api.patch>( + `${this.basePath}/agent-type/${agentTypeId}/order`, + { sectionOrders } + ); + return response.data.data; + } } export const profileSectionsService = new ProfileSectionsService();