diff --git a/apps/calendar/src/app/[locale]/(root)/navbar.tsx b/apps/calendar/src/app/[locale]/(root)/navbar.tsx index 97af4d7f39..218457a963 100644 --- a/apps/calendar/src/app/[locale]/(root)/navbar.tsx +++ b/apps/calendar/src/app/[locale]/(root)/navbar.tsx @@ -60,6 +60,7 @@ export default async function Navbar({ } separator={} onlyOnMobile={onlyOnMobile} + className="bg-background" /> ); } diff --git a/apps/calendar/src/app/[locale]/(root)/page.tsx b/apps/calendar/src/app/[locale]/(root)/page.tsx index f782c629d5..62ddf9c001 100644 --- a/apps/calendar/src/app/[locale]/(root)/page.tsx +++ b/apps/calendar/src/app/[locale]/(root)/page.tsx @@ -1,8 +1,11 @@ 'use client'; +import { DEV_MODE } from '@/constants/common'; import { useQuery, useQueryClient } from '@tanstack/react-query'; +import { Button } from '@tuturuuu/ui/button'; import { SmartCalendar } from '@tuturuuu/ui/legacy/calendar/smart-calendar'; import { useLocale, useTranslations } from 'next-intl'; +import Link from 'next/link'; export default function Home() { const t = useTranslations('calendar'); @@ -11,6 +14,11 @@ export default function Home() { return (
+ {DEV_MODE && ( + + + + )} ; + averageTaskSize: number; + largestGap: number; + utilizationRate: number; +} + +const getCategoryColor = (category: string) => { + switch (category) { + case 'work': + return 'bg-dynamic-blue/10 text-dynamic-blue border-dynamic-blue/30'; + case 'personal': + return 'bg-dynamic-green/10 text-dynamic-green border-dynamic-green/30'; + case 'meeting': + return 'bg-dynamic-orange/10 text-dynamic-orange border-dynamic-orange/30'; + default: + return 'bg-dynamic-gray/10 text-dynamic-gray border-dynamic-gray/30'; + } +}; + +export function AlgorithmInsights({ + tasks, + events, + logs, +}: AlgorithmInsightsProps) { + const metrics = useMemo((): SchedulingMetrics => { + const scheduledTaskIds = new Set(events.map((e) => e.taskId)); + const scheduledTasks = tasks.filter((t) => scheduledTaskIds.has(t.id)); + + const totalDuration = tasks.reduce((sum, task) => sum + task.duration, 0); + const scheduledDuration = events.reduce( + (sum, event) => + sum + event.range.end.diff(event.range.start, 'hour', true), + 0 + ); + + const splitTasks = new Set( + events + .filter((e) => e.partNumber && e.partNumber > 1) + .map((e) => e.taskId) + ).size; + + let deadlinesMet = 0; + let deadlinesMissed = 0; + + tasks.forEach((task) => { + if (task.deadline) { + const taskEvents = events.filter((e) => e.taskId === task.id); + const lastEvent = taskEvents.sort((a, b) => + b.range.end.diff(a.range.end) + )[0]; + + if (lastEvent) { + if (lastEvent.range.end.isAfter(task.deadline)) { + deadlinesMissed++; + } else { + deadlinesMet++; + } + } + } + }); + + const categoryDistribution = tasks.reduce( + (acc, task) => { + acc[task.category] = (acc[task.category] || 0) + 1; + return acc; + }, + {} as Record + ); + + const averageTaskSize = totalDuration / tasks.length || 0; + + // Calculate utilization rate (simplified) + const workingHours = 8; // Assume 8-hour work day + const utilizationRate = Math.min( + (scheduledDuration / workingHours) * 100, + 100 + ); + + return { + totalTasks: tasks.length, + scheduledTasks: scheduledTasks.length, + totalDuration, + scheduledDuration, + splitTasks, + deadlinesMet, + deadlinesMissed, + categoryDistribution, + averageTaskSize, + largestGap: 0, // TODO: Calculate actual gaps + utilizationRate, + }; + }, [tasks, events]); + + const algorithmConsiderations = useMemo(() => { + const considerations = []; + + // Deadline prioritization + const tasksWithDeadlines = tasks.filter((t) => t.deadline).length; + if (tasksWithDeadlines > 0) { + considerations.push({ + icon: TargetIcon, + title: 'Deadline Prioritization', + description: `${tasksWithDeadlines} tasks have deadlines. Algorithm prioritizes these tasks based on urgency.`, + impact: 'high', + }); + } + + // Task splitting analysis + const splittableTasks = tasks.filter( + (t) => t.maxDuration < t.duration + ).length; + if (splittableTasks > 0) { + considerations.push({ + icon: ZapIcon, + title: 'Task Splitting Strategy', + description: `${splittableTasks} tasks can be split. Algorithm balances between focus time and flexibility.`, + impact: 'medium', + }); + } + + // Category time management + const categories = Object.keys(metrics.categoryDistribution); + if (categories.length > 1) { + considerations.push({ + icon: LayersIcon, + title: 'Category-Based Scheduling', + description: `Tasks span ${categories.length} categories. Each category respects its specific time constraints.`, + impact: 'medium', + }); + } + + // Utilization optimization + considerations.push({ + icon: TrendingUpIcon, + title: 'Time Utilization', + description: `Current schedule achieves ${metrics.utilizationRate.toFixed(1)}% utilization of available time.`, + impact: metrics.utilizationRate > 80 ? 'high' : 'medium', + }); + + // Constraint satisfaction + const constraintViolations = logs.filter( + (log) => log.type === 'error' + ).length; + if (constraintViolations === 0) { + considerations.push({ + icon: CheckCircleIcon, + title: 'Constraint Satisfaction', + description: + 'All scheduling constraints are satisfied without conflicts.', + impact: 'high', + }); + } + + return considerations; + }, [tasks, metrics, logs]); + + if (tasks.length === 0) { + return ( + + + + + Algorithm Insights + + + Detailed analysis of scheduling decisions and optimizations + + + +
+ +

Add tasks and generate a schedule to see algorithm insights.

+
+
+
+ ); + } + + return ( +
+ {/* Algorithm Considerations */} + + + + + Algorithm Considerations + + + How the scheduling algorithm approaches your task arrangement + + + + {algorithmConsiderations.map((consideration, index) => { + const IconComponent = consideration.icon; + const impactColor = + consideration.impact === 'high' + ? 'text-dynamic-green' + : consideration.impact === 'medium' + ? 'text-dynamic-orange' + : 'text-dynamic-blue'; + + return ( +
+ +
+
+

{consideration.title}

+ + {consideration.impact} impact + +
+

+ {consideration.description} +

+
+
+ ); + })} +
+
+ + {/* Scheduling Metrics */} + + + Scheduling Metrics + + Quantitative analysis of your schedule optimization + + + + {/* Overview Stats */} +
+
+
+ {metrics.scheduledTasks}/{metrics.totalTasks} +
+
+ Tasks Scheduled +
+
+
+
+ {metrics.scheduledDuration.toFixed(1)}h +
+
+ Time Scheduled +
+
+
+
+ {metrics.splitTasks} +
+
Split Tasks
+
+
+
+ {metrics.utilizationRate.toFixed(0)}% +
+
Utilization
+
+
+ + + + {/* Deadline Performance */} + {(metrics.deadlinesMet > 0 || metrics.deadlinesMissed > 0) && ( +
+

+ + Deadline Performance +

+
+
+ Deadlines Met + + {metrics.deadlinesMet} + +
+
+ Deadlines Missed + + {metrics.deadlinesMissed} + +
+ +
+
+ )} + + {/* Category Distribution */} +
+

+ + Category Distribution +

+
+ {Object.entries(metrics.categoryDistribution).map( + ([category, count]) => ( +
+
+ + {category} + +
+ {count} tasks +
+ ) + )} +
+
+
+
+ + {/* Logs and Warnings */} + {logs.length > 0 && ( + + + + + Scheduling Insights & Warnings + + + Detailed feedback from the scheduling process + + + +
+ {logs.map((log, index) => ( + + {log.type === 'warning' && } + {log.type === 'error' && } + + {log.type.charAt(0).toUpperCase() + log.type.slice(1)} + + + {log.message} + + + ))} +
+
+
+ )} +
+ ); +} diff --git a/apps/calendar/src/app/[locale]/(root)/scheduler/components/ScheduleDisplay.tsx b/apps/calendar/src/app/[locale]/(root)/scheduler/components/ScheduleDisplay.tsx new file mode 100644 index 0000000000..738a0327af --- /dev/null +++ b/apps/calendar/src/app/[locale]/(root)/scheduler/components/ScheduleDisplay.tsx @@ -0,0 +1,303 @@ +'use client'; + +import type { Event } from '@tuturuuu/ai/scheduling/types'; +import { Badge } from '@tuturuuu/ui/badge'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@tuturuuu/ui/card'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import { + AlertTriangleIcon, + CalendarIcon, + ClockIcon, + SparklesIcon, + TrendingUpIcon, +} from 'lucide-react'; +import { useMemo } from 'react'; + +dayjs.extend(relativeTime); + +interface ScheduleDisplayProps { + events: Event[]; +} + +const getCategoryColor = (taskId: string) => { + // We can't get category from events directly, so we'll use a simple color scheme based on task ID + const hash = taskId.split('').reduce((a, b) => { + a = (a << 5) - a + b.charCodeAt(0); + return a & a; + }, 0); + + const colors = [ + 'bg-dynamic-blue/10 text-dynamic-blue border-dynamic-blue/30', + 'bg-dynamic-green/10 text-dynamic-green border-dynamic-green/30', + 'bg-dynamic-orange/10 text-dynamic-orange border-dynamic-orange/30', + 'bg-dynamic-purple/10 text-dynamic-purple border-dynamic-purple/30', + 'bg-dynamic-red/10 text-dynamic-red border-dynamic-red/30', + ]; + + return colors[Math.abs(hash) % colors.length]; +}; + +export function ScheduleDisplay({ events }: ScheduleDisplayProps) { + const groupedEvents = useMemo(() => { + return events.reduce( + (acc, event) => { + const date = event.range.start.format('YYYY-MM-DD'); + if (!acc[date]) { + acc[date] = []; + } + acc[date].push(event); + return acc; + }, + {} as Record + ); + }, [events]); + + const scheduleStats = useMemo(() => { + const totalDuration = events.reduce( + (sum, event) => + sum + event.range.end.diff(event.range.start, 'hour', true), + 0 + ); + + const uniqueTasks = new Set(events.map((e) => e.taskId)).size; + const splitTasks = new Set( + events + .filter((e) => e.partNumber && e.partNumber > 1) + .map((e) => e.taskId) + ).size; + + const overdueEvents = events.filter((e) => e.isPastDeadline).length; + + return { + totalDuration, + uniqueTasks, + splitTasks, + overdueEvents, + daysSpanned: Object.keys(groupedEvents).length, + }; + }, [events, groupedEvents]); + + if (events.length === 0) { + return ( + + + + + Your Schedule + + + Optimized task schedule with intelligent splitting + + + +
+ +

+ No Schedule Generated +

+

+ Add tasks and click "Generate Schedule" to see your + optimized timeline +

+
+
+
+ ); + } + + return ( +
+ {/* Schedule Overview */} + + + + + Schedule Overview + + Summary of your optimized schedule + + +
+
+
+ {events.length} +
+
Events
+
+
+
+ {scheduleStats.uniqueTasks} +
+
Tasks
+
+
+
+ {scheduleStats.totalDuration.toFixed(1)}h +
+
Total Time
+
+
+
+ {scheduleStats.splitTasks} +
+
Split Tasks
+
+
+
+ {scheduleStats.daysSpanned} +
+
Days
+
+
+
+
+ + {/* Schedule Timeline */} + + + + + Schedule Timeline + + + Your tasks organized by day and time + + + +
+ {Object.entries(groupedEvents) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([date, dailyEvents]) => { + const sortedEvents = dailyEvents.sort((a, b) => + a.range.start.diff(b.range.start) + ); + + const dayDuration = dailyEvents.reduce( + (sum, event) => + sum + event.range.end.diff(event.range.start, 'hour', true), + 0 + ); + + return ( +
+ {/* Day Header */} +
+
+

+ {dayjs(date).format('dddd, MMMM D')} +

+

+ {dailyEvents.length} events • {dayDuration.toFixed(1)}{' '} + hours +

+
+ + {dayjs(date).fromNow()} + +
+ + {/* Events Timeline */} +
+ {sortedEvents.map((event, eventIndex) => { + const duration = event.range.end.diff( + event.range.start, + 'hour', + true + ); + const nextEvent = sortedEvents[eventIndex + 1]; + const gap = nextEvent + ? nextEvent.range.start.diff( + event.range.end, + 'minute' + ) + : 0; + + return ( +
+ {/* Event Card */} +
+
+
+ {/* Event Header */} +
+ {event.isPastDeadline && ( + + + + + +

+ This task is scheduled past its + deadline +

+
+
+ )} +

+ {event.name} +

+ {event.partNumber && ( + + Part {event.partNumber}/ + {event.totalParts} + + )} +
+ + {/* Event Details */} +
+ + + {event.range.start.format('HH:mm')} -{' '} + {event.range.end.format('HH:mm')} + + {duration.toFixed(1)}h + + {event.taskId.split('-')[0]} + +
+
+
+
+ + {/* Gap Indicator */} + {gap > 0 && ( +
+
+ + {gap} minute break +
+
+ )} +
+ ); + })} +
+
+ ); + })} +
+
+
+
+ ); +} diff --git a/apps/calendar/src/app/[locale]/(root)/scheduler/components/TaskList.tsx b/apps/calendar/src/app/[locale]/(root)/scheduler/components/TaskList.tsx new file mode 100644 index 0000000000..5c9513b4e9 --- /dev/null +++ b/apps/calendar/src/app/[locale]/(root)/scheduler/components/TaskList.tsx @@ -0,0 +1,474 @@ +'use client'; + +import type { Event, Task } from '@tuturuuu/ai/scheduling/types'; +import { Badge } from '@tuturuuu/ui/badge'; +import { Button } from '@tuturuuu/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@tuturuuu/ui/card'; +import { Input } from '@tuturuuu/ui/input'; +import { Label } from '@tuturuuu/ui/label'; +import { Progress } from '@tuturuuu/ui/progress'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@tuturuuu/ui/select'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip'; +import dayjs from 'dayjs'; +import { + CalendarIcon, + CheckCircleIcon, + ClockIcon, + PlusIcon, + Trash2Icon, + ZapIcon, +} from 'lucide-react'; +import { useMemo } from 'react'; + +interface TaskListProps { + tasks: Task[]; + events: Event[]; + isScheduling: boolean; + onAddTask: () => void; + onUpdateTask: (id: string, updates: Partial) => void; + onDeleteTask: (id: string) => void; + onSchedule: () => void; +} + +const getCategoryColor = (category: 'work' | 'personal' | 'meeting') => { + switch (category) { + case 'work': + return 'bg-dynamic-blue/10 text-dynamic-blue border-dynamic-blue/30'; + case 'personal': + return 'bg-dynamic-green/10 text-dynamic-green border-dynamic-green/30'; + case 'meeting': + return 'bg-dynamic-orange/10 text-dynamic-orange border-dynamic-orange/30'; + default: + return 'bg-dynamic-gray/10 text-dynamic-gray border-dynamic-gray/30'; + } +}; + +const getCategoryIcon = (category: 'work' | 'personal' | 'meeting') => { + switch (category) { + case 'work': + return '💼'; + case 'personal': + return '🏠'; + case 'meeting': + return '👥'; + default: + return '📋'; + } +}; + +export function TaskList({ + tasks, + events, + isScheduling, + onAddTask, + onUpdateTask, + onDeleteTask, + onSchedule, +}: TaskListProps) { + const taskProgress = useMemo(() => { + const progressMap = new Map< + string, + { completed: number; remaining: number } + >(); + + tasks.forEach((task) => { + const taskEvents = events.filter((event) => event.taskId === task.id); + const completedTime = taskEvents.reduce((sum, event) => { + return sum + event.range.end.diff(event.range.start, 'hour', true); + }, 0); + + progressMap.set(task.id, { + completed: completedTime, + remaining: Math.max(0, task.duration - completedTime), + }); + }); + + return progressMap; + }, [tasks, events]); + + const totalDuration = tasks.reduce((sum, task) => sum + task.duration, 0); + const completedTasks = tasks.filter((task) => { + const progress = taskProgress.get(task.id); + return progress && progress.remaining === 0; + }).length; + + const getDeadlineStatus = (deadline?: dayjs.Dayjs) => { + if (!deadline) return null; + + const now = dayjs(); + const hoursUntil = deadline.diff(now, 'hour', true); + + if (hoursUntil < 0) { + return { type: 'overdue', text: 'Overdue', color: 'text-destructive' }; + } else if (hoursUntil < 24) { + return { + type: 'urgent', + text: `${Math.round(hoursUntil)}h left`, + color: 'text-dynamic-orange', + }; + } else if (hoursUntil < 72) { + return { + type: 'soon', + text: `${Math.round(hoursUntil / 24)}d left`, + color: 'text-dynamic-yellow', + }; + } else { + return { + type: 'later', + text: deadline.format('MMM D'), + color: 'text-muted-foreground', + }; + } + }; + + return ( +
+ {/* Header with Stats */} + + +
+
+ + + Task Management + + + Organize and track your tasks with intelligent scheduling + +
+ +
+
+ +
+
+
+ {tasks.length} +
+
Total Tasks
+
+
+
+ {completedTasks} +
+
Completed
+
+
+
+ {totalDuration}h +
+
Total Time
+
+
+
+ {tasks.filter((t) => t.deadline).length} +
+
+ With Deadlines +
+
+
+
+
+ + {/* Tasks List */} + + + Your Tasks + + Manage task details and constraints for intelligent scheduling + + + + {tasks.length === 0 ? ( +
+ +

No Tasks Yet

+

+ Add some tasks or load a template to get started with scheduling +

+ +
+ ) : ( +
+ {tasks.map((task) => { + const progress = taskProgress.get(task.id); + const progressPercentage = progress + ? (progress.completed / task.duration) * 100 + : 0; + const deadlineStatus = getDeadlineStatus(task.deadline); + const isCompleted = progress?.remaining === 0; + + return ( +
+ {/* Task Header */} +
+
+
+ + {getCategoryIcon(task.category)} + + + onUpdateTask(task.id, { name: e.target.value }) + } + className={`h-auto border-none bg-transparent p-0 text-lg font-semibold focus-visible:ring-0 ${ + isCompleted + ? 'text-muted-foreground line-through' + : '' + }`} + /> + {isCompleted && ( + + )} +
+ + {/* Progress Bar */} + {progress && ( +
+
+ + Progress: {progress.completed.toFixed(1)}h /{' '} + {task.duration}h + + + {Math.round(progressPercentage)}% + +
+ + {progress.remaining > 0 && ( +
+ + + {progress.remaining.toFixed(1)}h remaining + +
+ )} +
+ )} + + {/* Tags */} +
+ + {task.category} + + + {deadlineStatus && ( + + + + + {deadlineStatus.text} + + + +

+ Deadline:{' '} + {task.deadline?.format('MMM D, YYYY HH:mm')} +

+
+
+ )} + + {task.maxDuration < task.duration && ( + + + + + Splittable + + + +

+ This task can be split into smaller chunks +

+
+
+ )} +
+
+ + +
+ + {/* Task Details */} +
+
+ + + onUpdateTask(task.id, { + duration: parseFloat(e.target.value), + }) + } + className="text-sm" + /> +
+ +
+ + + onUpdateTask(task.id, { + minDuration: parseFloat(e.target.value), + }) + } + className="text-sm" + /> +
+ +
+ + + onUpdateTask(task.id, { + maxDuration: parseFloat(e.target.value), + }) + } + className="text-sm" + /> +
+ +
+ + +
+
+ + {/* Deadline */} +
+ + + onUpdateTask(task.id, { + deadline: e.target.value + ? dayjs(e.target.value) + : undefined, + }) + } + className="text-sm" + min={dayjs().format('YYYY-MM-DDTHH:mm')} + /> +
+
+ ); + })} +
+ )} +
+ + {tasks.length > 0 && ( +
+ +
+ )} +
+
+ ); +} diff --git a/apps/calendar/src/app/[locale]/(root)/scheduler/components/TaskModal.tsx b/apps/calendar/src/app/[locale]/(root)/scheduler/components/TaskModal.tsx new file mode 100644 index 0000000000..d0066ff3a6 --- /dev/null +++ b/apps/calendar/src/app/[locale]/(root)/scheduler/components/TaskModal.tsx @@ -0,0 +1,354 @@ +'use client'; + +import type { Task } from '@tuturuuu/ai/scheduling/types'; +import { Button } from '@tuturuuu/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@tuturuuu/ui/dialog'; +import { Input } from '@tuturuuu/ui/input'; +import { Label } from '@tuturuuu/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@tuturuuu/ui/select'; +import { Separator } from '@tuturuuu/ui/separator'; +import { Textarea } from '@tuturuuu/ui/textarea'; +import dayjs from 'dayjs'; +import { CalendarIcon, ClockIcon, PlusIcon, TagIcon } from 'lucide-react'; +import { useState } from 'react'; + +interface TaskModalProps { + isOpen: boolean; + onClose: () => void; + onAddTask: (task: Omit) => void; +} + +const categoryOptions = [ + { + value: 'work', + label: 'Work', + icon: '💼', + description: 'Professional tasks and projects', + }, + { + value: 'personal', + label: 'Personal', + icon: '🏠', + description: 'Personal activities and hobbies', + }, + { + value: 'meeting', + label: 'Meeting', + icon: '👥', + description: 'Meetings and collaborative sessions', + }, +] as const; + +export function TaskModal({ isOpen, onClose, onAddTask }: TaskModalProps) { + const [formData, setFormData] = useState({ + name: '', + description: '', + duration: 1, + minDuration: 0.5, + maxDuration: 2, + category: 'work' as 'work' | 'personal' | 'meeting', + deadline: '', + priority: 'medium' as 'low' | 'medium' | 'high', + }); + + const [errors, setErrors] = useState>({}); + + const validateForm = () => { + const newErrors: Record = {}; + + if (!formData.name.trim()) { + newErrors.name = 'Task name is required'; + } + + if (formData.duration <= 0) { + newErrors.duration = 'Duration must be greater than 0'; + } + + if (formData.minDuration <= 0) { + newErrors.minDuration = 'Minimum duration must be greater than 0'; + } + + if (formData.maxDuration <= 0) { + newErrors.maxDuration = 'Maximum duration must be greater than 0'; + } + + if (formData.minDuration > formData.maxDuration) { + newErrors.minDuration = 'Minimum duration cannot be greater than maximum'; + } + + if (formData.duration < formData.minDuration) { + newErrors.duration = 'Duration cannot be less than minimum duration'; + } + + if (formData.deadline && dayjs(formData.deadline).isBefore(dayjs())) { + newErrors.deadline = 'Deadline cannot be in the past'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + const newTask: Omit = { + name: formData.name, + duration: formData.duration, + minDuration: formData.minDuration, + maxDuration: formData.maxDuration, + category: formData.category, + deadline: formData.deadline ? dayjs(formData.deadline) : undefined, + }; + + onAddTask(newTask); + handleClose(); + }; + + const handleClose = () => { + setFormData({ + name: '', + description: '', + duration: 1, + minDuration: 0.5, + maxDuration: 2, + category: 'work', + deadline: '', + priority: 'medium', + }); + setErrors({}); + onClose(); + }; + + const updateFormData = (field: string, value: string | number) => { + setFormData((prev) => ({ ...prev, [field]: value })); + if (errors[field]) { + setErrors((prev) => ({ ...prev, [field]: '' })); + } + }; + + return ( + + + + + + Add New Task + + + Create a new task with scheduling constraints and preferences. + + + +
+ {/* Basic Information */} +
+
+ + updateFormData('name', e.target.value)} + className={errors.name ? 'border-destructive' : ''} + /> + {errors.name && ( +

{errors.name}

+ )} +
+ +
+ +