fix
This commit is contained in:
491
src/app/dashboard/profile-sections/page.tsx
Normal file
491
src/app/dashboard/profile-sections/page.tsx
Normal file
@@ -0,0 +1,491 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
profileSectionsService,
|
||||
ProfileSection,
|
||||
CreateSectionDto,
|
||||
UpdateSectionDto,
|
||||
getErrorMessage,
|
||||
} from '@/services';
|
||||
|
||||
type ModalType = 'create' | 'edit' | 'delete' | null;
|
||||
|
||||
export default function ProfileSectionsPage() {
|
||||
const router = useRouter();
|
||||
const [sections, setSections] = useState<ProfileSection[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const [showInactive, setShowInactive] = useState(true);
|
||||
|
||||
// Modal state
|
||||
const [modalType, setModalType] = useState<ModalType>(null);
|
||||
const [selectedSection, setSelectedSection] = useState<ProfileSection | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [modalError, setModalError] = useState('');
|
||||
|
||||
// Form state
|
||||
const [formData, setFormData] = useState<CreateSectionDto>({
|
||||
name: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
isActive: true,
|
||||
isGlobal: false,
|
||||
sortOrder: 0,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSections();
|
||||
}, [showInactive]);
|
||||
|
||||
const fetchSections = async () => {
|
||||
setIsLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const data = await profileSectionsService.getAll(showInactive);
|
||||
setSections(data);
|
||||
} catch (err) {
|
||||
const errorMessage = getErrorMessage(err);
|
||||
setError(errorMessage);
|
||||
if (errorMessage.includes('Unauthorized')) {
|
||||
router.push('/login');
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
icon: '',
|
||||
isActive: true,
|
||||
isGlobal: false,
|
||||
sortOrder: 0,
|
||||
});
|
||||
setModalError('');
|
||||
setModalType('create');
|
||||
};
|
||||
|
||||
const openEditModal = (section: ProfileSection) => {
|
||||
setSelectedSection(section);
|
||||
setFormData({
|
||||
name: section.name,
|
||||
description: section.description || '',
|
||||
icon: section.icon || '',
|
||||
isActive: section.isActive,
|
||||
isGlobal: section.isGlobal,
|
||||
sortOrder: section.sortOrder,
|
||||
});
|
||||
setModalError('');
|
||||
setModalType('edit');
|
||||
};
|
||||
|
||||
const openDeleteModal = (section: ProfileSection) => {
|
||||
setSelectedSection(section);
|
||||
setModalError('');
|
||||
setModalType('delete');
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalType(null);
|
||||
setSelectedSection(null);
|
||||
setModalError('');
|
||||
};
|
||||
|
||||
const handleCreate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setModalError('');
|
||||
try {
|
||||
await profileSectionsService.create(formData);
|
||||
closeModal();
|
||||
fetchSections();
|
||||
} catch (err) {
|
||||
setModalError(getErrorMessage(err));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedSection) return;
|
||||
setIsSubmitting(true);
|
||||
setModalError('');
|
||||
try {
|
||||
await profileSectionsService.update(selectedSection.id, formData as UpdateSectionDto);
|
||||
closeModal();
|
||||
fetchSections();
|
||||
} catch (err) {
|
||||
setModalError(getErrorMessage(err));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!selectedSection) return;
|
||||
setIsSubmitting(true);
|
||||
setModalError('');
|
||||
try {
|
||||
await profileSectionsService.delete(selectedSection.id);
|
||||
closeModal();
|
||||
fetchSections();
|
||||
} catch (err) {
|
||||
setModalError(getErrorMessage(err));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleStatus = async (section: ProfileSection) => {
|
||||
try {
|
||||
await profileSectionsService.update(section.id, { isActive: !section.isActive });
|
||||
fetchSections();
|
||||
} catch (err) {
|
||||
setError(getErrorMessage(err));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Title */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Profile Sections</h1>
|
||||
<p className="text-gray-500">Manage profile sections and their fields for agent types</p>
|
||||
</div>
|
||||
|
||||
{/* Sections Table */}
|
||||
<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">
|
||||
<label className="flex items-center space-x-2 text-sm text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showInactive}
|
||||
onChange={(e) => setShowInactive(e.target.checked)}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<span>Show Inactive</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
onClick={fetchSections}
|
||||
className="px-4 py-2 border border-gray-300 hover:bg-gray-50 text-gray-700 text-sm font-medium rounded-lg transition-colors flex items-center"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
onClick={openCreateModal}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors flex items-center"
|
||||
>
|
||||
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Section
|
||||
</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>
|
||||
)}
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
</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="M4 6h16M4 10h16M4 14h16M4 18h16" />
|
||||
</svg>
|
||||
<p className="mt-2 text-gray-500">No profile sections found</p>
|
||||
<button
|
||||
onClick={openCreateModal}
|
||||
className="mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors"
|
||||
>
|
||||
Create First Section
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Section
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Description
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Fields
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Agent Types
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Global
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sections.map((section) => (
|
||||
<tr key={section.id} className="hover:bg-gray-50">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
{section.icon && (
|
||||
<span className="mr-2 text-lg">{section.icon}</span>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-sm font-medium text-gray-900">{section.name}</div>
|
||||
<div className="text-xs text-gray-500">{section.slug}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="text-sm text-gray-500 max-w-xs truncate">
|
||||
{section.description || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<Link
|
||||
href={`/dashboard/profile-sections/${section.id}`}
|
||||
className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200"
|
||||
>
|
||||
{section._count?.fields || 0} fields
|
||||
<svg className="ml-1 w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="text-sm text-gray-500">
|
||||
{section._count?.agentTypeSections || 0} assigned
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
section.isGlobal
|
||||
? 'bg-purple-100 text-purple-800'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{section.isGlobal ? 'Global' : 'Specific'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<button
|
||||
onClick={() => toggleStatus(section)}
|
||||
className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full cursor-pointer transition-colors ${
|
||||
section.isActive
|
||||
? 'bg-green-100 text-green-800 hover:bg-green-200'
|
||||
: 'bg-gray-100 text-gray-800 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{section.isActive ? 'Active' : 'Inactive'}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<Link
|
||||
href={`/dashboard/profile-sections/${section.id}`}
|
||||
className="text-blue-600 hover:text-blue-900 mr-4"
|
||||
>
|
||||
Manage
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => openEditModal(section)}
|
||||
className="text-gray-600 hover:text-gray-900 mr-4"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openDeleteModal(section)}
|
||||
className="text-red-600 hover:text-red-900"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
{(modalType === 'create' || modalType === 'edit') && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{modalType === 'create' ? 'Create Profile Section' : 'Edit Profile Section'}
|
||||
</h3>
|
||||
</div>
|
||||
<form onSubmit={modalType === 'create' ? handleCreate : handleUpdate}>
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
{modalError && (
|
||||
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-700 text-sm">{modalError}</p>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Name <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-900 bg-white placeholder:text-gray-500"
|
||||
placeholder="e.g., Location, Experience, Education"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-900 bg-white placeholder:text-gray-500"
|
||||
placeholder="Brief description of this section"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Icon (emoji)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.icon}
|
||||
onChange={(e) => setFormData({ ...formData, icon: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-900 bg-white placeholder:text-gray-500"
|
||||
placeholder="e.g., location_on"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Sort Order
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.sortOrder}
|
||||
onChange={(e) => setFormData({ ...formData, sortOrder: parseInt(e.target.value) || 0 })}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-gray-900 bg-white"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-6">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||||
className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<label htmlFor="isActive" className="ml-2 text-sm text-gray-700">
|
||||
Active
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isGlobal"
|
||||
checked={formData.isGlobal}
|
||||
onChange={(e) => setFormData({ ...formData, isGlobal: e.target.checked })}
|
||||
className="rounded border-gray-300 text-purple-600 focus:ring-purple-500"
|
||||
/>
|
||||
<label htmlFor="isGlobal" className="ml-2 text-sm text-gray-700">
|
||||
Global (all agent types)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={closeModal}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? 'Saving...' : modalType === 'create' ? 'Create' : 'Save Changes'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Delete Confirmation Modal */}
|
||||
{modalType === 'delete' && selectedSection && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<div className="px-6 py-4 border-b border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Delete Profile Section</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4">
|
||||
{modalError && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-700 text-sm">{modalError}</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-gray-600">
|
||||
Are you sure you want to delete <span className="font-semibold">{selectedSection.name}</span>?
|
||||
</p>
|
||||
{(selectedSection._count?.fields && selectedSection._count.fields > 0) && (
|
||||
<p className="mt-2 text-amber-600 text-sm">
|
||||
Warning: This section has {selectedSection._count.fields} field(s). They will also be deleted.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-gray-200 flex justify-end space-x-3">
|
||||
<button
|
||||
onClick={closeModal}
|
||||
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? 'Deleting...' : 'Delete'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user