Replace src folder with new version
This commit is contained in:
953
src/components/pages/SectionConfigurationManager.tsx
Normal file
953
src/components/pages/SectionConfigurationManager.tsx
Normal file
@@ -0,0 +1,953 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user