update profile section
This commit is contained in:
349
src/app/dashboard/agent-types/[id]/sections/page.tsx
Normal file
349
src/app/dashboard/agent-types/[id]/sections/page.tsx
Normal file
@@ -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<AgentTypeSectionsResponse | null>(null);
|
||||||
|
const [sections, setSections] = useState<SectionWithOrder[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(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 (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{/* Breadcrumb */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<nav className="flex items-center space-x-2 text-sm text-gray-500">
|
||||||
|
<Link href="/dashboard/agent-types" className="hover:text-blue-600">
|
||||||
|
Agent Types
|
||||||
|
</Link>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-gray-900 font-medium">{data?.agentType.name || 'Loading...'}</span>
|
||||||
|
<span>/</span>
|
||||||
|
<span className="text-gray-900">Sections</span>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Page Title */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">
|
||||||
|
Manage Sections for {data?.agentType.name}
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-500">
|
||||||
|
Customize the order and requirements of profile sections for this agent type.
|
||||||
|
Drag to reorder or use the arrow buttons.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sections List */}
|
||||||
|
<div className="bg-white rounded-lg shadow">
|
||||||
|
<div className="px-6 py-4 border-b border-gray-200">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span className="text-sm text-gray-600">
|
||||||
|
{sections.length} section{sections.length !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
{hasChanges && (
|
||||||
|
<span className="px-2 py-1 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||||
|
Unsaved Changes
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
{hasChanges && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={handleReset}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={isSaving}
|
||||||
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 flex items-center"
|
||||||
|
>
|
||||||
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||||
|
</svg>
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Save Order'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="px-6 py-4 bg-red-50 border-b border-red-200">
|
||||||
|
<p className="text-red-700 text-sm">{error}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{sections.length === 0 ? (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||||
|
</svg>
|
||||||
|
<p className="mt-2 text-gray-500">No sections assigned to this agent type</p>
|
||||||
|
<Link
|
||||||
|
href="/dashboard/profile-sections"
|
||||||
|
className="mt-4 inline-block px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
Go to Profile Sections
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-gray-200">
|
||||||
|
{sections.map((section, index) => (
|
||||||
|
<div
|
||||||
|
key={section.id}
|
||||||
|
draggable
|
||||||
|
onDragStart={() => 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' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{/* Drag Handle */}
|
||||||
|
<div className="text-gray-400 cursor-grab">
|
||||||
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M8 6a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm0 6a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm0 6a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm8-12a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm0 6a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm0 6a2 2 0 1 1-4 0 2 2 0 0 1 4 0z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Order Number */}
|
||||||
|
<div className="w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 text-gray-600 text-sm font-medium">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section Info */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="font-medium text-gray-900">{section.name}</span>
|
||||||
|
{section.isGlobal && (
|
||||||
|
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-purple-100 text-purple-800">
|
||||||
|
Global
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{section.isSystem && (
|
||||||
|
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
|
||||||
|
System
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{section.hasCustomOrder && (
|
||||||
|
<span className="px-2 py-0.5 text-xs font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||||
|
Custom Order
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{section.description || 'No description'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{/* Required Toggle */}
|
||||||
|
<label className="flex items-center space-x-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={section.isRequired}
|
||||||
|
onChange={() => toggleRequired(index)}
|
||||||
|
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-600">Required</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Move Buttons */}
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<button
|
||||||
|
onClick={() => moveSection(index, 'up')}
|
||||||
|
disabled={index === 0}
|
||||||
|
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
title="Move up"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => moveSection(index, 'down')}
|
||||||
|
disabled={index === sections.length - 1}
|
||||||
|
className="p-1.5 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded disabled:opacity-30 disabled:cursor-not-allowed"
|
||||||
|
title="Move down"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Info Box */}
|
||||||
|
<div className="mt-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
|
<h3 className="text-sm font-semibold text-blue-800 mb-2">How Section Ordering Works</h3>
|
||||||
|
<ul className="text-sm text-blue-700 space-y-1">
|
||||||
|
<li><strong>Global sections</strong> (purple badge) are automatically included for all agent types.</li>
|
||||||
|
<li><strong>System sections</strong> (blue badge) cannot be deleted, only hidden.</li>
|
||||||
|
<li><strong>Custom Order</strong> (yellow badge) indicates this agent type has a custom order for that section.</li>
|
||||||
|
<li>Drag sections or use the arrow buttons to change the display order.</li>
|
||||||
|
<li>Mark sections as <strong>Required</strong> to enforce completion by agents.</li>
|
||||||
|
<li>Click <strong>Save Order</strong> to apply your changes.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -271,6 +271,12 @@ export default function AgentTypesPage() {
|
|||||||
<div className="text-sm text-gray-500">{agentType._count?.agents || 0}</div>
|
<div className="text-sm text-gray-500">{agentType._count?.agents || 0}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
|
<button
|
||||||
|
onClick={() => router.push(`/dashboard/agent-types/${agentType.id}/sections`)}
|
||||||
|
className="text-purple-600 hover:text-purple-900 mr-4"
|
||||||
|
>
|
||||||
|
Sections
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => openEditModal(agentType)}
|
onClick={() => openEditModal(agentType)}
|
||||||
className="text-blue-600 hover:text-blue-900 mr-4"
|
className="text-blue-600 hover:text-blue-900 mr-4"
|
||||||
|
|||||||
@@ -282,6 +282,15 @@ export default function SectionDetailPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleGlobal = async (newIsGlobal: boolean) => {
|
||||||
|
try {
|
||||||
|
await profileSectionsService.update(sectionId, { isGlobal: newIsGlobal });
|
||||||
|
fetchData();
|
||||||
|
} catch (err) {
|
||||||
|
setError(getErrorMessage(err));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getFieldTypeLabel = (type: FieldType) => {
|
const getFieldTypeLabel = (type: FieldType) => {
|
||||||
const found = FIELD_TYPES.find((ft) => ft.value === type);
|
const found = FIELD_TYPES.find((ft) => ft.value === type);
|
||||||
return found?.label || type;
|
return found?.label || type;
|
||||||
@@ -469,7 +478,7 @@ export default function SectionDetailPage() {
|
|||||||
<div className="bg-white rounded-lg shadow">
|
<div className="bg-white rounded-lg shadow">
|
||||||
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
<div className="px-6 py-4 border-b border-gray-200 flex justify-between items-center">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Agent Types</h2>
|
<h2 className="text-lg font-semibold text-gray-900">Agent Types</h2>
|
||||||
{!section.isGlobal && availableAgentTypes.length > 0 && (
|
{availableAgentTypes.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={openAssignModal}
|
onClick={openAssignModal}
|
||||||
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
|
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||||
@@ -479,17 +488,38 @@ export default function SectionDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{section.isGlobal ? (
|
{/* Global toggle */}
|
||||||
<div className="text-center py-4">
|
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg mb-4">
|
||||||
<span className="px-3 py-1.5 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
|
<div>
|
||||||
Available to all agent types
|
<p className="font-medium text-gray-900">Available to all agent types</p>
|
||||||
</span>
|
<p className="text-xs text-gray-500">
|
||||||
<p className="mt-3 text-sm text-gray-500">
|
{section.isGlobal
|
||||||
This is a global section and appears for all agent types.
|
? 'This section appears for all agent types by default'
|
||||||
|
: 'Only assigned agent types will see this section'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : section.agentTypeSections && section.agentTypeSections.length > 0 ? (
|
<button
|
||||||
|
onClick={() => handleToggleGlobal(!section.isGlobal)}
|
||||||
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 ${
|
||||||
|
section.isGlobal ? 'bg-purple-600' : 'bg-gray-200'
|
||||||
|
}`}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={section.isGlobal}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||||
|
section.isGlobal ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Specific agent type assignments */}
|
||||||
|
{section.agentTypeSections && section.agentTypeSections.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
|
<p className="text-xs text-gray-500 font-medium uppercase tracking-wide mb-2">
|
||||||
|
Specific Assignments
|
||||||
|
</p>
|
||||||
{section.agentTypeSections.map((ats) => {
|
{section.agentTypeSections.map((ats) => {
|
||||||
const agentType = agentTypes.find((at) => at.id === ats.agentTypeId);
|
const agentType = agentTypes.find((at) => at.id === ats.agentTypeId);
|
||||||
return (
|
return (
|
||||||
@@ -518,13 +548,17 @@ export default function SectionDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center py-4">
|
<div className="text-center py-4">
|
||||||
<p className="text-gray-500 text-sm">Not assigned to any agent type</p>
|
<p className="text-gray-500 text-sm">
|
||||||
|
{section.isGlobal
|
||||||
|
? 'No specific agent type configurations yet'
|
||||||
|
: 'Not assigned to any agent type'}
|
||||||
|
</p>
|
||||||
{availableAgentTypes.length > 0 && (
|
{availableAgentTypes.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={openAssignModal}
|
onClick={openAssignModal}
|
||||||
className="mt-3 text-blue-600 hover:underline text-sm"
|
className="mt-3 text-blue-600 hover:underline text-sm"
|
||||||
>
|
>
|
||||||
Assign to agent type
|
{section.isGlobal ? 'Add specific configuration' : 'Assign to agent type'}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import SectionIcon from '@/components/SectionIcon';
|
||||||
import {
|
import {
|
||||||
profileSectionsService,
|
profileSectionsService,
|
||||||
ProfileSection,
|
ProfileSection,
|
||||||
@@ -254,7 +255,9 @@ export default function ProfileSectionsPage() {
|
|||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{section.icon && (
|
{section.icon && (
|
||||||
<span className="mr-2 text-lg">{section.icon}</span>
|
<span className="mr-3 text-gray-500">
|
||||||
|
<SectionIcon name={section.icon} className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-gray-900">{section.name}</div>
|
<div className="text-sm font-medium text-gray-900">{section.name}</div>
|
||||||
@@ -280,7 +283,11 @@ export default function ProfileSectionsPage() {
|
|||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{section._count?.agentTypeSections || 0} assigned
|
{section.isGlobal ? (
|
||||||
|
<span className="text-purple-600 font-medium">All (Global)</span>
|
||||||
|
) : (
|
||||||
|
`${section._count?.agentTypeSections || 0} assigned`
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4 whitespace-nowrap">
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
|||||||
101
src/components/SectionIcon.tsx
Normal file
101
src/components/SectionIcon.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
interface SectionIconProps {
|
||||||
|
name: string | null | undefined;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map icon names to SVG paths
|
||||||
|
const iconPaths: Record<string, string> = {
|
||||||
|
// 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 (
|
||||||
|
<svg
|
||||||
|
className={className}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
{paths.map((p, i) => (
|
||||||
|
<path
|
||||||
|
key={i}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d={p}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -37,6 +37,8 @@ export type {
|
|||||||
UpdateSectionDto,
|
UpdateSectionDto,
|
||||||
AssignSectionDto,
|
AssignSectionDto,
|
||||||
UpdateAssignmentDto,
|
UpdateAssignmentDto,
|
||||||
|
SectionOrderItem,
|
||||||
|
AgentTypeSectionsResponse,
|
||||||
} from './profile-sections.service';
|
} from './profile-sections.service';
|
||||||
|
|
||||||
// Profile Fields Service
|
// Profile Fields Service
|
||||||
|
|||||||
@@ -66,6 +66,26 @@ export interface UpdateAssignmentDto {
|
|||||||
isRequired?: boolean;
|
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
|
// Profile Sections Service
|
||||||
class ProfileSectionsService {
|
class ProfileSectionsService {
|
||||||
private basePath = '/profile-sections';
|
private basePath = '/profile-sections';
|
||||||
@@ -121,6 +141,26 @@ class ProfileSectionsService {
|
|||||||
async removeFromAgentType(sectionId: string, agentTypeId: string): Promise<void> {
|
async removeFromAgentType(sectionId: string, agentTypeId: string): Promise<void> {
|
||||||
await api.delete(`${this.basePath}/${sectionId}/assignment/${agentTypeId}`);
|
await api.delete(`${this.basePath}/${sectionId}/assignment/${agentTypeId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agent Type Sections methods
|
||||||
|
async getSectionsForAgentType(agentTypeId: string, includeFields = true): Promise<AgentTypeSectionsResponse> {
|
||||||
|
const url = includeFields
|
||||||
|
? `${this.basePath}/agent-type/${agentTypeId}`
|
||||||
|
: `${this.basePath}/agent-type/${agentTypeId}?includeFields=false`;
|
||||||
|
const response = await api.get<ApiResponse<AgentTypeSectionsResponse>>(url);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSectionOrderForAgentType(
|
||||||
|
agentTypeId: string,
|
||||||
|
sectionOrders: SectionOrderItem[]
|
||||||
|
): Promise<{ success: boolean; updated: number }> {
|
||||||
|
const response = await api.patch<ApiResponse<{ success: boolean; updated: number }>>(
|
||||||
|
`${this.basePath}/agent-type/${agentTypeId}/order`,
|
||||||
|
{ sectionOrders }
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const profileSectionsService = new ProfileSectionsService();
|
export const profileSectionsService = new ProfileSectionsService();
|
||||||
|
|||||||
Reference in New Issue
Block a user