953 lines
36 KiB
TypeScript
953 lines
36 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { AuthenticatedLayout } from '../layout/AuthenticatedLayout';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
|
import { Button } from '../ui/button';
|
|
import { Badge } from '../ui/badge';
|
|
import { Input } from '../ui/input';
|
|
import { Label } from '../ui/label';
|
|
import { Textarea } from '../ui/textarea';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '../ui/table';
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '../ui/select';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from '../ui/dropdown-menu';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '../ui/dialog';
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from '../ui/sheet';
|
|
import { Checkbox } from '../ui/checkbox';
|
|
import { Separator } from '../ui/separator';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
import { toast } from "sonner@2.0.3";
|
|
import {
|
|
Plus,
|
|
Search,
|
|
MoreHorizontal,
|
|
Edit,
|
|
Settings,
|
|
Copy,
|
|
Trash2,
|
|
ArrowUpDown,
|
|
FileText,
|
|
Target,
|
|
CheckCircle,
|
|
XCircle,
|
|
AlertCircle,
|
|
Save,
|
|
X,
|
|
Info
|
|
} from 'lucide-react';
|
|
|
|
interface SectionConfigurationManagerProps {
|
|
onNavigate: (route: string) => void;
|
|
onLogout: () => void;
|
|
user: any;
|
|
}
|
|
|
|
// Mock section configuration templates
|
|
const sectionConfigTemplates = [
|
|
{
|
|
id: "template_leadership_360",
|
|
name: "Leadership 360 - Standard",
|
|
description: "Standard 360-degree leadership assessment configuration",
|
|
questionType: "Ipsative",
|
|
sections: [
|
|
{ name: "Strategic Thinking", numberOfStatements: 4, order: 1 },
|
|
{ name: "Communication Skills", numberOfStatements: 3, order: 2 },
|
|
{ name: "Team Leadership", numberOfStatements: 5, order: 3 },
|
|
{ name: "Results Orientation", numberOfStatements: 4, order: 4 }
|
|
],
|
|
usage: 12,
|
|
createdBy: "Dr. Rajesh Mehta",
|
|
createdAt: "2025-08-01T10:00:00Z"
|
|
},
|
|
{
|
|
id: "template_communication_likert",
|
|
name: "Communication Assessment - Likert",
|
|
description: "Comprehensive communication skills assessment using Likert scales",
|
|
questionType: "Likert",
|
|
sections: [
|
|
{ name: "Verbal Communication", numberOfOptions: 5, order: 1 },
|
|
{ name: "Written Communication", numberOfOptions: 7, order: 2 },
|
|
{ name: "Active Listening", numberOfOptions: 5, order: 3 },
|
|
{ name: "Non-verbal Communication", numberOfOptions: 5, order: 4 }
|
|
],
|
|
usage: 8,
|
|
createdBy: "Prof. Priya Sinha",
|
|
createdAt: "2025-08-05T14:30:00Z"
|
|
},
|
|
{
|
|
id: "template_innovation_tf",
|
|
name: "Innovation Mindset - True/False",
|
|
description: "Innovation capabilities assessment using binary choices",
|
|
questionType: "True/False",
|
|
sections: [
|
|
{ name: "Creative Thinking", order: 1 },
|
|
{ name: "Risk Taking", order: 2 },
|
|
{ name: "Problem Solving", order: 3 },
|
|
{ name: "Adaptability", order: 4 }
|
|
],
|
|
usage: 5,
|
|
createdBy: "Dr. Amit Sharma",
|
|
createdAt: "2025-08-10T09:15:00Z"
|
|
},
|
|
{
|
|
id: "template_strategy_matching",
|
|
name: "Strategic Thinking - Matching",
|
|
description: "Strategic capabilities assessment using matching exercises",
|
|
questionType: "Matching",
|
|
sections: [
|
|
{ name: "Strategic Analysis", order: 1 },
|
|
{ name: "Strategy Formulation", order: 2 },
|
|
{ name: "Strategy Implementation", order: 3 }
|
|
],
|
|
usage: 3,
|
|
createdBy: "Prof. Sunita Agarwal",
|
|
createdAt: "2025-08-12T16:45:00Z"
|
|
},
|
|
{
|
|
id: "template_culture_descriptive",
|
|
name: "Team Culture Survey - Descriptive",
|
|
description: "Open-ended team culture and engagement assessment",
|
|
questionType: "Descriptive",
|
|
sections: [
|
|
{ name: "Culture Anchors", order: 1 },
|
|
{ name: "Core Values", order: 2 },
|
|
{ name: "Team Communication", order: 3 },
|
|
{ name: "Collaboration", order: 4 },
|
|
{ name: "Employee Engagement", order: 5 }
|
|
],
|
|
usage: 7,
|
|
createdBy: "Dr. Neha Shah",
|
|
createdAt: "2025-08-15T11:20:00Z"
|
|
}
|
|
];
|
|
|
|
// Section configuration rules based on question type
|
|
const questionTypeConfig = {
|
|
"Ipsative": {
|
|
label: "Ipsative (Forced Choice)",
|
|
description: "Users choose between ranked statements",
|
|
configField: "numberOfStatements",
|
|
configLabel: "Number of Statements",
|
|
defaultValue: 4,
|
|
minValue: 2,
|
|
maxValue: 10,
|
|
helpText: "Statements users will rank in order of preference"
|
|
},
|
|
"Likert": {
|
|
label: "Likert Scale",
|
|
description: "Users rate items on a scale",
|
|
configField: "numberOfOptions",
|
|
configLabel: "Scale Points",
|
|
defaultValue: 5,
|
|
minValue: 3,
|
|
maxValue: 10,
|
|
helpText: "Number of points on the rating scale (e.g., 1-5, 1-7)"
|
|
},
|
|
"True/False": {
|
|
label: "True/False",
|
|
description: "Binary choice questions",
|
|
configField: null,
|
|
configLabel: "No additional configuration",
|
|
defaultValue: null,
|
|
helpText: "Binary questions don't require additional configuration"
|
|
},
|
|
"Matching": {
|
|
label: "Matching Exercise",
|
|
description: "Match items from different lists",
|
|
configField: null,
|
|
configLabel: "No additional configuration",
|
|
defaultValue: null,
|
|
helpText: "Matching exercises are configured per question"
|
|
},
|
|
"Descriptive": {
|
|
label: "Open Text",
|
|
description: "Free-form text responses",
|
|
configField: null,
|
|
configLabel: "No additional configuration",
|
|
defaultValue: null,
|
|
helpText: "Descriptive questions don't require section-level configuration"
|
|
}
|
|
};
|
|
|
|
export function SectionConfigurationManager({ onNavigate, onLogout, user }: SectionConfigurationManagerProps) {
|
|
// State management
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [questionTypeFilter, setQuestionTypeFilter] = useState("all");
|
|
const [selectedTemplates, setSelectedTemplates] = useState<string[]>([]);
|
|
|
|
// Dialog and drawer states
|
|
const [isCreateTemplateOpen, setIsCreateTemplateOpen] = useState(false);
|
|
const [isEditTemplateOpen, setIsEditTemplateOpen] = useState(false);
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
const [isCloneTemplateOpen, setIsCloneTemplateOpen] = useState(false);
|
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
|
|
|
// Template management states
|
|
const [editingTemplate, setEditingTemplate] = useState<any>(null);
|
|
const [deletingTemplate, setDeletingTemplate] = useState<any>(null);
|
|
const [cloningTemplate, setCloningTemplate] = useState<any>(null);
|
|
|
|
// Form states
|
|
const [newTemplate, setNewTemplate] = useState({
|
|
name: "",
|
|
description: "",
|
|
questionType: "",
|
|
sections: []
|
|
});
|
|
|
|
const [editSections, setEditSections] = useState<any[]>([]);
|
|
|
|
const breadcrumbs = [
|
|
{ label: "Admin", href: "/dashboard" },
|
|
{ label: "Content", href: "/content" },
|
|
{ label: "Profilers", href: "/profilers" },
|
|
{ label: "Section Configuration" }
|
|
];
|
|
|
|
// Filter templates
|
|
const filteredTemplates = sectionConfigTemplates.filter(template => {
|
|
const matchesSearch =
|
|
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
template.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
template.createdBy.toLowerCase().includes(searchTerm.toLowerCase());
|
|
|
|
const matchesQuestionType = questionTypeFilter === "all" || template.questionType === questionTypeFilter;
|
|
|
|
return matchesSearch && matchesQuestionType;
|
|
});
|
|
|
|
// Question type filter options
|
|
const questionTypeOptions = [
|
|
{ value: "all", label: "All Question Types", count: sectionConfigTemplates.length },
|
|
{ value: "Ipsative", label: "Ipsative", count: sectionConfigTemplates.filter(t => t.questionType === "Ipsative").length },
|
|
{ value: "Likert", label: "Likert", count: sectionConfigTemplates.filter(t => t.questionType === "Likert").length },
|
|
{ value: "True/False", label: "True/False", count: sectionConfigTemplates.filter(t => t.questionType === "True/False").length },
|
|
{ value: "Matching", label: "Matching", count: sectionConfigTemplates.filter(t => t.questionType === "Matching").length },
|
|
{ value: "Descriptive", label: "Descriptive", count: sectionConfigTemplates.filter(t => t.questionType === "Descriptive").length }
|
|
];
|
|
|
|
// Utility functions
|
|
const getQuestionTypeConfig = (questionType: string) => {
|
|
return questionTypeConfig[questionType as keyof typeof questionTypeConfig] || questionTypeConfig["True/False"];
|
|
};
|
|
|
|
const addSection = () => {
|
|
const config = getQuestionTypeConfig(newTemplate.questionType);
|
|
const newSection: any = {
|
|
id: `section_${Date.now()}`,
|
|
name: "",
|
|
order: editSections.length + 1
|
|
};
|
|
|
|
if (config.configField) {
|
|
newSection[config.configField] = config.defaultValue;
|
|
}
|
|
|
|
setEditSections([...editSections, newSection]);
|
|
};
|
|
|
|
const updateSection = (index: number, field: string, value: any) => {
|
|
const updated = [...editSections];
|
|
updated[index] = { ...updated[index], [field]: value };
|
|
setEditSections(updated);
|
|
};
|
|
|
|
const removeSection = (index: number) => {
|
|
const updated = editSections.filter((_, i) => i !== index);
|
|
// Reorder sections
|
|
const reordered = updated.map((section, i) => ({ ...section, order: i + 1 }));
|
|
setEditSections(reordered);
|
|
};
|
|
|
|
const moveSection = (index: number, direction: 'up' | 'down') => {
|
|
if ((direction === 'up' && index === 0) || (direction === 'down' && index === editSections.length - 1)) {
|
|
return;
|
|
}
|
|
|
|
const updated = [...editSections];
|
|
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
|
|
|
// Swap sections
|
|
[updated[index], updated[targetIndex]] = [updated[targetIndex], updated[index]];
|
|
|
|
// Update order
|
|
updated[index].order = index + 1;
|
|
updated[targetIndex].order = targetIndex + 1;
|
|
|
|
setEditSections(updated);
|
|
};
|
|
|
|
// Action handlers
|
|
const handleCreateTemplate = () => {
|
|
setNewTemplate({
|
|
name: "",
|
|
description: "",
|
|
questionType: "",
|
|
sections: []
|
|
});
|
|
setEditSections([]);
|
|
setIsCreateTemplateOpen(true);
|
|
};
|
|
|
|
const handleEditTemplate = (template: any) => {
|
|
setEditingTemplate(template);
|
|
setNewTemplate({
|
|
name: template.name,
|
|
description: template.description,
|
|
questionType: template.questionType,
|
|
sections: template.sections
|
|
});
|
|
setEditSections([...template.sections]);
|
|
setIsEditTemplateOpen(true);
|
|
};
|
|
|
|
const handleCloneTemplate = (template: any) => {
|
|
setCloningTemplate(template);
|
|
setNewTemplate({
|
|
name: `${template.name} (Copy)`,
|
|
description: template.description,
|
|
questionType: template.questionType,
|
|
sections: template.sections
|
|
});
|
|
setEditSections([...template.sections]);
|
|
setIsCloneTemplateOpen(true);
|
|
};
|
|
|
|
const handleDeleteTemplate = (template: any) => {
|
|
setDeletingTemplate(template);
|
|
setIsDeleteDialogOpen(true);
|
|
};
|
|
|
|
const saveTemplate = () => {
|
|
if (!newTemplate.name.trim()) {
|
|
toast.error("Template name is required");
|
|
return;
|
|
}
|
|
|
|
if (!newTemplate.questionType) {
|
|
toast.error("Question type is required");
|
|
return;
|
|
}
|
|
|
|
if (editSections.length === 0) {
|
|
toast.error("At least one section is required");
|
|
return;
|
|
}
|
|
|
|
// Validate sections
|
|
for (const section of editSections) {
|
|
if (!section.name.trim()) {
|
|
toast.error("All sections must have names");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const templateData = {
|
|
...newTemplate,
|
|
sections: editSections
|
|
};
|
|
|
|
if (isEditTemplateOpen) {
|
|
toast.success(`Template "${newTemplate.name}" updated successfully`);
|
|
setIsEditTemplateOpen(false);
|
|
} else {
|
|
toast.success(`Template "${newTemplate.name}" created successfully`);
|
|
setIsCreateTemplateOpen(false);
|
|
setIsCloneTemplateOpen(false);
|
|
}
|
|
|
|
// Reset form
|
|
setNewTemplate({ name: "", description: "", questionType: "", sections: [] });
|
|
setEditSections([]);
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
if (deletingTemplate) {
|
|
toast.success(`Template "${deletingTemplate.name}" deleted successfully`);
|
|
setIsDeleteDialogOpen(false);
|
|
setDeletingTemplate(null);
|
|
}
|
|
};
|
|
|
|
const handleBulkDelete = () => {
|
|
toast.success(`${selectedTemplates.length} templates deleted successfully`);
|
|
setSelectedTemplates([]);
|
|
setIsBulkDeleteDialogOpen(false);
|
|
};
|
|
|
|
// Filter chip component
|
|
const FilterChip = ({ label, count, active, onClick }: { label: string; count?: number; active: boolean; onClick: () => void }) => (
|
|
<button
|
|
onClick={onClick}
|
|
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-full border transition-colors min-h-[44px] ${
|
|
active
|
|
? 'bg-[var(--color-brand-primary)] text-white border-[var(--color-brand-primary)]'
|
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
|
}`}
|
|
>
|
|
{label}
|
|
{count !== undefined && (
|
|
<span className={`px-1.5 py-0.5 rounded-full text-xs ${
|
|
active ? 'bg-white/20 text-white' : 'bg-gray-100 text-gray-600'
|
|
}`}>
|
|
{count}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
|
|
return (
|
|
<AuthenticatedLayout
|
|
currentRoute="/admin/section-configuration"
|
|
user={user}
|
|
onNavigate={onNavigate}
|
|
onLogout={onLogout}
|
|
breadcrumbs={breadcrumbs}
|
|
>
|
|
<div className="p-6 max-w-7xl mx-auto space-y-6">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-[var(--color-brand-black)]">Section Configuration Templates</h1>
|
|
<p className="text-gray-600 mt-1">
|
|
Manage reusable section configurations for profiler assessments
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => onNavigate('/profilers')}
|
|
className="min-h-[44px]"
|
|
>
|
|
Back to Profilers
|
|
</Button>
|
|
<Button
|
|
onClick={handleCreateTemplate}
|
|
className="min-h-[44px]"
|
|
style={{ backgroundColor: 'var(--color-brand-primary)' }}
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
New Template
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter Bar */}
|
|
<Card>
|
|
<CardContent className="p-6 space-y-4">
|
|
{/* Search */}
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex-1 min-w-[300px]">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
|
<Input
|
|
placeholder="Search templates by name, description, or creator"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 min-h-[44px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter Chips */}
|
|
<div>
|
|
<Label className="text-gray-700 mb-2 block">Question Type</Label>
|
|
<div className="flex flex-wrap gap-2">
|
|
{questionTypeOptions.map(option => (
|
|
<FilterChip
|
|
key={option.value}
|
|
label={option.label}
|
|
count={option.count}
|
|
active={questionTypeFilter === option.value}
|
|
onClick={() => setQuestionTypeFilter(option.value)}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Bulk Action Bar */}
|
|
{selectedTemplates.length > 0 && (
|
|
<Card className="border-[var(--color-brand-primary)]">
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<span>
|
|
{selectedTemplates.length} template{selectedTemplates.length > 1 ? 's' : ''} selected
|
|
</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setSelectedTemplates([])}
|
|
className="text-gray-500"
|
|
>
|
|
Clear selection
|
|
</Button>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setIsBulkDeleteDialogOpen(true)}
|
|
className="min-h-[44px] text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete Selected
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Templates Table */}
|
|
{filteredTemplates.length === 0 ? (
|
|
<Card>
|
|
<CardContent className="p-12">
|
|
<div className="text-center">
|
|
<Settings className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
|
<h3 className="mb-2">No section configuration templates found</h3>
|
|
<p className="text-gray-600 mb-6">
|
|
Create reusable section configurations to standardize your profiler assessments.
|
|
</p>
|
|
<Button
|
|
onClick={handleCreateTemplate}
|
|
style={{ backgroundColor: 'var(--color-brand-primary)' }}
|
|
className="min-h-[44px]"
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Create First Template
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader className="bg-gray-50 sticky top-0">
|
|
<TableRow className="border-b">
|
|
<TableHead className="w-12">
|
|
<Checkbox
|
|
checked={selectedTemplates.length === filteredTemplates.length}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
setSelectedTemplates(filteredTemplates.map(t => t.id));
|
|
} else {
|
|
setSelectedTemplates([]);
|
|
}
|
|
}}
|
|
/>
|
|
</TableHead>
|
|
<TableHead>Template Name</TableHead>
|
|
<TableHead>Question Type</TableHead>
|
|
<TableHead>Sections</TableHead>
|
|
<TableHead>Usage</TableHead>
|
|
<TableHead>Created By</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead className="w-12">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredTemplates.map((template) => (
|
|
<TableRow key={template.id} className="border-b hover:bg-gray-50 min-h-[44px]">
|
|
<TableCell>
|
|
<Checkbox
|
|
checked={selectedTemplates.includes(template.id)}
|
|
onCheckedChange={(checked) => {
|
|
if (checked) {
|
|
setSelectedTemplates([...selectedTemplates, template.id]);
|
|
} else {
|
|
setSelectedTemplates(selectedTemplates.filter(id => id !== template.id));
|
|
}
|
|
}}
|
|
/>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div>
|
|
<div className="font-medium">{template.name}</div>
|
|
<div className="text-sm text-gray-500">{template.description}</div>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="secondary" className="text-xs">
|
|
{template.questionType}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger>
|
|
<div className="text-sm">
|
|
{template.sections.length} section{template.sections.length > 1 ? 's' : ''}
|
|
</div>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<div className="space-y-1">
|
|
{template.sections.map((section, index) => (
|
|
<div key={index} className="text-sm">
|
|
{section.name}
|
|
{section.numberOfStatements && ` (${section.numberOfStatements} statements)`}
|
|
{section.numberOfOptions && ` (${section.numberOfOptions}-point scale)`}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="text-sm">
|
|
Used in {template.usage} profiler{template.usage > 1 ? 's' : ''}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>{template.createdBy}</TableCell>
|
|
<TableCell className="text-gray-600">
|
|
{new Date(template.createdAt).toLocaleDateString('en-GB', {
|
|
day: '2-digit',
|
|
month: 'short',
|
|
year: 'numeric'
|
|
})}
|
|
</TableCell>
|
|
<TableCell>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="sm" className="h-8 w-8 p-0">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="w-48">
|
|
<DropdownMenuItem onClick={() => handleEditTemplate(template)}>
|
|
<Edit className="h-4 w-4 mr-2" />
|
|
Edit Template
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={() => handleCloneTemplate(template)}>
|
|
<Copy className="h-4 w-4 mr-2" />
|
|
Clone Template
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onClick={() => handleDeleteTemplate(template)}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete Template
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Create/Edit Template Dialog */}
|
|
<Dialog open={isCreateTemplateOpen || isEditTemplateOpen || isCloneTemplateOpen} onOpenChange={(open) => {
|
|
if (!open) {
|
|
setIsCreateTemplateOpen(false);
|
|
setIsEditTemplateOpen(false);
|
|
setIsCloneTemplateOpen(false);
|
|
setNewTemplate({ name: "", description: "", questionType: "", sections: [] });
|
|
setEditSections([]);
|
|
}
|
|
}}>
|
|
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{isEditTemplateOpen ? 'Edit Template' : isCloneTemplateOpen ? 'Clone Template' : 'Create New Template'}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
{isEditTemplateOpen ? 'Modify the section configuration template' :
|
|
isCloneTemplateOpen ? 'Create a copy of the section configuration template' :
|
|
'Create a reusable section configuration template for profiler assessments'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-6">
|
|
{/* Basic Information */}
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<Label htmlFor="template-name">Template Name</Label>
|
|
<Input
|
|
id="template-name"
|
|
value={newTemplate.name}
|
|
onChange={(e) => setNewTemplate({ ...newTemplate, name: e.target.value })}
|
|
placeholder="Enter template name"
|
|
className="min-h-[44px]"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Label htmlFor="question-type">Question Type</Label>
|
|
<Select
|
|
value={newTemplate.questionType}
|
|
onValueChange={(value) => {
|
|
setNewTemplate({ ...newTemplate, questionType: value });
|
|
// Reset sections when question type changes
|
|
setEditSections([]);
|
|
}}
|
|
disabled={isEditTemplateOpen}
|
|
>
|
|
<SelectTrigger className="min-h-[44px]">
|
|
<SelectValue placeholder="Select question type" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{Object.entries(questionTypeConfig).map(([type, config]) => (
|
|
<SelectItem key={type} value={type}>
|
|
<div>
|
|
<div>{config.label}</div>
|
|
<div className="text-sm text-gray-500">{config.description}</div>
|
|
</div>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="template-description">Description</Label>
|
|
<Textarea
|
|
id="template-description"
|
|
value={newTemplate.description}
|
|
onChange={(e) => setNewTemplate({ ...newTemplate, description: e.target.value })}
|
|
placeholder="Describe this template and its intended use"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
{/* Question Type Configuration Info */}
|
|
{newTemplate.questionType && (
|
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
<div className="flex items-start gap-3">
|
|
<Info className="h-5 w-5 text-blue-600 mt-0.5 flex-shrink-0" />
|
|
<div>
|
|
<h4 className="font-medium text-blue-900">
|
|
{getQuestionTypeConfig(newTemplate.questionType).label} Configuration
|
|
</h4>
|
|
<p className="text-sm text-blue-700 mt-1">
|
|
{getQuestionTypeConfig(newTemplate.questionType).helpText}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Sections Configuration */}
|
|
{newTemplate.questionType && (
|
|
<div>
|
|
<div className="flex items-center justify-between mb-4">
|
|
<Label>Sections Configuration</Label>
|
|
<Button
|
|
onClick={addSection}
|
|
size="sm"
|
|
className="min-h-[44px]"
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add Section
|
|
</Button>
|
|
</div>
|
|
|
|
{editSections.length === 0 ? (
|
|
<div className="text-center py-8 border-2 border-dashed border-gray-300 rounded-lg">
|
|
<FileText className="h-8 w-8 mx-auto mb-2 text-gray-400" />
|
|
<p className="text-gray-500">No sections added yet</p>
|
|
<p className="text-sm text-gray-400">Click "Add Section" to get started</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{editSections.map((section, index) => {
|
|
const typeConfig = getQuestionTypeConfig(newTemplate.questionType);
|
|
return (
|
|
<div key={section.id || index} className="border rounded-lg p-4 bg-gray-50">
|
|
<div className="flex items-center gap-3 mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => moveSection(index, 'up')}
|
|
disabled={index === 0}
|
|
className="h-8 w-8 p-0"
|
|
>
|
|
<ArrowUpDown className="h-4 w-4" />
|
|
</Button>
|
|
<span className="text-sm font-medium text-gray-500">
|
|
Section {section.order}
|
|
</span>
|
|
</div>
|
|
<div className="flex-1">
|
|
<Input
|
|
value={section.name}
|
|
onChange={(e) => updateSection(index, 'name', e.target.value)}
|
|
placeholder="Section name"
|
|
className="min-h-[44px]"
|
|
/>
|
|
</div>
|
|
{typeConfig.configField && (
|
|
<div className="w-32">
|
|
<Input
|
|
type="number"
|
|
value={section[typeConfig.configField] || typeConfig.defaultValue}
|
|
onChange={(e) => updateSection(index, typeConfig.configField!, parseInt(e.target.value))}
|
|
min={typeConfig.minValue}
|
|
max={typeConfig.maxValue}
|
|
placeholder={typeConfig.configLabel}
|
|
className="min-h-[44px]"
|
|
/>
|
|
</div>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeSection(index)}
|
|
className="h-8 w-8 p-0 text-red-600 hover:text-red-700"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
{typeConfig.configField && (
|
|
<div className="text-xs text-gray-500">
|
|
{typeConfig.configLabel}: {section[typeConfig.configField] || typeConfig.defaultValue}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => {
|
|
setIsCreateTemplateOpen(false);
|
|
setIsEditTemplateOpen(false);
|
|
setIsCloneTemplateOpen(false);
|
|
}}
|
|
className="min-h-[44px]"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={saveTemplate}
|
|
className="min-h-[44px]"
|
|
style={{ backgroundColor: 'var(--color-brand-primary)' }}
|
|
>
|
|
<Save className="h-4 w-4 mr-2" />
|
|
{isEditTemplateOpen ? 'Update Template' : 'Save Template'}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Delete Template</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete "{deletingTemplate?.name}"? This action cannot be undone.
|
|
{deletingTemplate?.usage > 0 && (
|
|
<div className="mt-2 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
|
<div className="flex items-center gap-2">
|
|
<AlertCircle className="h-4 w-4 text-yellow-600" />
|
|
<span className="text-sm text-yellow-800">
|
|
This template is currently used in {deletingTemplate.usage} profiler{deletingTemplate.usage > 1 ? 's' : ''}.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsDeleteDialogOpen(false)}
|
|
className="min-h-[44px]"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={confirmDelete}
|
|
className="min-h-[44px]"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete Template
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Bulk Delete Confirmation Dialog */}
|
|
<Dialog open={isBulkDeleteDialogOpen} onOpenChange={setIsBulkDeleteDialogOpen}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Delete Selected Templates</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to delete {selectedTemplates.length} selected template{selectedTemplates.length > 1 ? 's' : ''}?
|
|
This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setIsBulkDeleteDialogOpen(false)}
|
|
className="min-h-[44px]"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
onClick={handleBulkDelete}
|
|
className="min-h-[44px]"
|
|
>
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete {selectedTemplates.length} Template{selectedTemplates.length > 1 ? 's' : ''}
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
);
|
|
} |