update profile section

This commit is contained in:
pradeepkumar
2026-01-24 15:33:11 +05:30
parent 758a84b20e
commit 37612dd001
7 changed files with 552 additions and 13 deletions

View 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>
);
}

View File

@@ -271,6 +271,12 @@ export default function AgentTypesPage() {
<div className="text-sm text-gray-500">{agentType._count?.agents || 0}</div>
</td>
<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
onClick={() => openEditModal(agentType)}
className="text-blue-600 hover:text-blue-900 mr-4"

View File

@@ -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 found = FIELD_TYPES.find((ft) => ft.value === type);
return found?.label || type;
@@ -469,7 +478,7 @@ export default function SectionDetailPage() {
<div className="bg-white rounded-lg shadow">
<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>
{!section.isGlobal && availableAgentTypes.length > 0 && (
{availableAgentTypes.length > 0 && (
<button
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"
@@ -479,17 +488,38 @@ export default function SectionDetailPage() {
)}
</div>
<div className="p-6">
{section.isGlobal ? (
<div className="text-center py-4">
<span className="px-3 py-1.5 bg-purple-100 text-purple-800 rounded-full text-sm font-medium">
Available to all agent types
</span>
<p className="mt-3 text-sm text-gray-500">
This is a global section and appears for all agent types.
{/* Global toggle */}
<div className="flex items-center justify-between p-3 bg-gray-50 rounded-lg mb-4">
<div>
<p className="font-medium text-gray-900">Available to all agent types</p>
<p className="text-xs text-gray-500">
{section.isGlobal
? 'This section appears for all agent types by default'
: 'Only assigned agent types will see this section'}
</p>
</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">
<p className="text-xs text-gray-500 font-medium uppercase tracking-wide mb-2">
Specific Assignments
</p>
{section.agentTypeSections.map((ats) => {
const agentType = agentTypes.find((at) => at.id === ats.agentTypeId);
return (
@@ -518,13 +548,17 @@ export default function SectionDetailPage() {
</div>
) : (
<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 && (
<button
onClick={openAssignModal}
className="mt-3 text-blue-600 hover:underline text-sm"
>
Assign to agent type
{section.isGlobal ? 'Add specific configuration' : 'Assign to agent type'}
</button>
)}
</div>

View File

@@ -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() {
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
{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 className="text-sm font-medium text-gray-900">{section.name}</div>
@@ -280,7 +283,11 @@ export default function ProfileSectionsPage() {
</td>
<td className="px-6 py-4 whitespace-nowrap">
<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>
</td>
<td className="px-6 py-4 whitespace-nowrap">