655 lines
26 KiB
TypeScript
655 lines
26 KiB
TypeScript
import React, { useState } from 'react'
|
||
import { motion, AnimatePresence } from 'framer-motion'
|
||
import {
|
||
DndContext,
|
||
DragEndEvent,
|
||
DragOverlay,
|
||
DragStartEvent,
|
||
PointerSensor,
|
||
useSensor,
|
||
useSensors,
|
||
closestCorners,
|
||
useDroppable
|
||
} from '@dnd-kit/core'
|
||
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable'
|
||
import { useSortable } from '@dnd-kit/sortable'
|
||
import { CSS } from '@dnd-kit/utilities'
|
||
import {
|
||
HiPlus,
|
||
HiXMark,
|
||
HiClock,
|
||
HiChatBubbleLeftRight,
|
||
HiPaperClip,
|
||
HiTrash
|
||
} from 'react-icons/hi2'
|
||
import dayjs from 'dayjs'
|
||
import 'dayjs/locale/tr'
|
||
import { mockTasks, Task } from '../../../mocks/mockIntranetData'
|
||
import { Badge } from '@/components/ui'
|
||
|
||
dayjs.locale('tr')
|
||
|
||
type TaskStatus = 'todo' | 'in-progress' | 'review' | 'done'
|
||
|
||
// Droppable Column Component
|
||
interface DroppableColumnProps {
|
||
id: TaskStatus
|
||
children: React.ReactNode
|
||
}
|
||
|
||
const DroppableColumn: React.FC<DroppableColumnProps> = ({ id, children }) => {
|
||
const { setNodeRef } = useDroppable({ id })
|
||
|
||
return (
|
||
<div ref={setNodeRef} >
|
||
{children}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
// Sortable Task Card Component
|
||
interface SortableTaskCardProps {
|
||
task: Task
|
||
onTaskClick: (task: Task) => void
|
||
getPriorityColor: (priority: string) => string
|
||
getPriorityLabel: (priority: string) => string
|
||
isOverdue: (date: Date | string) => boolean
|
||
}
|
||
|
||
const SortableTaskCard: React.FC<SortableTaskCardProps> = ({
|
||
task,
|
||
onTaskClick,
|
||
getPriorityColor,
|
||
getPriorityLabel,
|
||
isOverdue
|
||
}) => {
|
||
const {
|
||
attributes,
|
||
listeners,
|
||
setNodeRef,
|
||
transform,
|
||
transition,
|
||
isDragging
|
||
} = useSortable({ id: task.id })
|
||
|
||
const style = {
|
||
transform: CSS.Transform.toString(transform),
|
||
transition,
|
||
opacity: isDragging ? 0.5 : 1
|
||
}
|
||
|
||
const overdue = isOverdue(task.dueDate) && task.status !== 'done'
|
||
|
||
return (
|
||
<div
|
||
ref={setNodeRef}
|
||
style={style}
|
||
{...attributes}
|
||
{...listeners}
|
||
className={`bg-white dark:bg-gray-800 rounded-lg p-3 sm:p-4 border-2 cursor-move hover:shadow-lg transition-all ${
|
||
overdue
|
||
? 'border-red-300 dark:border-red-700'
|
||
: 'border-gray-200 dark:border-gray-700'
|
||
} ${isDragging ? 'shadow-2xl ring-4 ring-blue-500/50' : ''}`}
|
||
onClick={(e) => {
|
||
if (!(e.target as HTMLElement).closest('[data-no-click]')) {
|
||
onTaskClick(task)
|
||
}
|
||
}}
|
||
>
|
||
<div className="flex items-start justify-between mb-2 sm:mb-3">
|
||
<span className={`px-2 py-1 text-xs font-medium rounded border ${getPriorityColor(task.priority)}`}>
|
||
{getPriorityLabel(task.priority)}
|
||
</span>
|
||
{overdue && (
|
||
<span className="text-xs text-red-600 dark:text-red-400 font-medium">
|
||
⚠️ Gecikmiş
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
<h4 className="font-semibold text-gray-900 dark:text-white mb-2">
|
||
{task.title}
|
||
</h4>
|
||
|
||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2">
|
||
{task.description}
|
||
</p>
|
||
|
||
<div className="mb-3">
|
||
<span className="inline-flex items-center px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs">
|
||
📁 {task.project}
|
||
</span>
|
||
</div>
|
||
|
||
{task.labels.length > 0 && (
|
||
<div className="flex flex-wrap gap-1 mb-3">
|
||
{task.labels.map((label, idx) => (
|
||
<span
|
||
key={idx}
|
||
className="px-2 py-0.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs rounded"
|
||
>
|
||
{label}
|
||
</span>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
<div className="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-700">
|
||
<div className="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||
<div className="flex items-center gap-1">
|
||
<HiClock className="w-4 h-4" />
|
||
{dayjs(task.dueDate).format('DD MMM')}
|
||
</div>
|
||
{task.comments > 0 && (
|
||
<div className="flex items-center gap-1">
|
||
<HiChatBubbleLeftRight className="w-4 h-4" />
|
||
{task.comments}
|
||
</div>
|
||
)}
|
||
{task.attachments && task.attachments.length > 0 && (
|
||
<div className="flex items-center gap-1">
|
||
<HiPaperClip className="w-4 h-4" />
|
||
{task.attachments.length}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex -space-x-2">
|
||
{task.assignedTo.slice(0, 3).map((assignee, idx) => (
|
||
<img
|
||
key={idx}
|
||
src={assignee.avatar}
|
||
alt={assignee.fullName}
|
||
className="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800"
|
||
title={assignee.fullName}
|
||
/>
|
||
))}
|
||
{task.assignedTo.length > 3 && (
|
||
<div className="w-6 h-6 rounded-full border-2 border-white dark:border-gray-800 bg-gray-300 dark:bg-gray-600 flex items-center justify-center text-xs font-medium text-gray-700 dark:text-gray-300">
|
||
+{task.assignedTo.length - 3}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const TasksModule: React.FC = () => {
|
||
const [tasks, setTasks] = useState<Task[]>(mockTasks)
|
||
const [selectedTask, setSelectedTask] = useState<Task | null>(null)
|
||
const [activeId, setActiveId] = useState<string | null>(null)
|
||
const [showNewTaskModal, setShowNewTaskModal] = useState(false)
|
||
const [newTaskColumn, setNewTaskColumn] = useState<TaskStatus>('todo')
|
||
|
||
const sensors = useSensors(
|
||
useSensor(PointerSensor, {
|
||
activationConstraint: {
|
||
distance: 8,
|
||
},
|
||
})
|
||
)
|
||
|
||
const columns: { id: TaskStatus; title: string; icon: string; color: string }[] = [
|
||
{ id: 'todo', title: 'Yapılacak', icon: '📋', color: 'gray' },
|
||
{ id: 'in-progress', title: 'Devam Ediyor', icon: '⚙️', color: 'blue' },
|
||
{ id: 'review', title: 'İncelemede', icon: '👀', color: 'yellow' },
|
||
{ id: 'done', title: 'Tamamlandı', icon: '✅', color: 'green' }
|
||
]
|
||
|
||
const handleDragStart = (event: DragStartEvent) => {
|
||
setActiveId(event.active.id as string)
|
||
}
|
||
|
||
const handleDragEnd = (event: DragEndEvent) => {
|
||
const { active, over } = event
|
||
setActiveId(null)
|
||
|
||
if (!over) return
|
||
|
||
const taskId = active.id as string
|
||
|
||
// over.id could be either a column id or a task id
|
||
// If it's a column id (from DroppableColumn), use it directly
|
||
// If it's a task id, find that task's column
|
||
let newStatus: TaskStatus
|
||
|
||
const overColumn = columns.find(col => col.id === over.id)
|
||
if (overColumn) {
|
||
newStatus = overColumn.id
|
||
} else {
|
||
// over.id is a task, find its column
|
||
const overTask = tasks.find(t => t.id === over.id)
|
||
if (!overTask) return
|
||
newStatus = overTask.status
|
||
}
|
||
|
||
// Update task status
|
||
setTasks(prevTasks =>
|
||
prevTasks.map(task =>
|
||
task.id === taskId ? { ...task, status: newStatus } : task
|
||
)
|
||
)
|
||
|
||
if (selectedTask?.id === taskId) {
|
||
setSelectedTask(prev => prev ? { ...prev, status: newStatus } : null)
|
||
}
|
||
}
|
||
|
||
const handleStatusChange = (taskId: string, newStatus: TaskStatus) => {
|
||
setTasks(prevTasks =>
|
||
prevTasks.map(task =>
|
||
task.id === taskId ? { ...task, status: newStatus } : task
|
||
)
|
||
)
|
||
|
||
if (selectedTask?.id === taskId) {
|
||
setSelectedTask(prev => prev ? { ...prev, status: newStatus } : null)
|
||
}
|
||
}
|
||
|
||
const handleAddTask = (status: TaskStatus) => {
|
||
setNewTaskColumn(status)
|
||
setShowNewTaskModal(true)
|
||
}
|
||
|
||
const handleCreateTask = (title: string, description: string) => {
|
||
const newTask: Task = {
|
||
id: `task-${Date.now()}`,
|
||
title,
|
||
description,
|
||
project: 'Genel',
|
||
assignedTo: [mockTasks[0].assignedTo[0]], // Default assignee
|
||
assignedBy: mockTasks[0].assignedBy,
|
||
priority: 'medium',
|
||
status: newTaskColumn,
|
||
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days from now
|
||
createdAt: new Date(),
|
||
labels: [],
|
||
comments: 0
|
||
}
|
||
|
||
setTasks(prev => [...prev, newTask])
|
||
setShowNewTaskModal(false)
|
||
}
|
||
|
||
const handleDeleteTask = (taskId: string) => {
|
||
if (window.confirm('Bu görevi silmek istediğinizden emin misiniz?')) {
|
||
setTasks(prevTasks => prevTasks.filter(task => task.id !== taskId))
|
||
setSelectedTask(null)
|
||
}
|
||
}
|
||
|
||
const getTasksByStatus = (status: TaskStatus) => {
|
||
return tasks.filter((task: Task) => task.status === status)
|
||
}
|
||
|
||
const getPriorityColor = (priority: string) => {
|
||
const colors = {
|
||
low: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-gray-200 dark:border-gray-600',
|
||
medium: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-300 border-blue-200 dark:border-blue-700',
|
||
high: 'bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-300 border-orange-200 dark:border-orange-700',
|
||
urgent: 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-300 border-red-200 dark:border-red-700'
|
||
}
|
||
return colors[priority as keyof typeof colors]
|
||
}
|
||
|
||
const getPriorityLabel = (priority: string) => {
|
||
const labels = {
|
||
low: 'Düşük',
|
||
medium: 'Orta',
|
||
high: 'Yüksek',
|
||
urgent: '🔥 Acil'
|
||
}
|
||
return labels[priority as keyof typeof labels]
|
||
}
|
||
|
||
const isOverdue = (dueDate: Date | string) => {
|
||
return dayjs(dueDate).isBefore(dayjs(), 'day')
|
||
}
|
||
|
||
return (
|
||
<DndContext
|
||
sensors={sensors}
|
||
collisionDetection={closestCorners}
|
||
onDragStart={handleDragStart}
|
||
onDragEnd={handleDragEnd}
|
||
>
|
||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 p-3 sm:p-4 md:p-6">
|
||
<div className="max-w-[1600px] mx-auto space-y-4 md:space-y-6">
|
||
{/* Header */}
|
||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||
<div>
|
||
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white">
|
||
Görev & Proje Yönetimi
|
||
</h1>
|
||
<p className="text-sm sm:text-base text-gray-600 dark:text-gray-400 mt-1">
|
||
Görevleri Kanban board ile yönetin
|
||
</p>
|
||
</div>
|
||
<div className="flex items-center gap-3">
|
||
<div className="flex items-center gap-2 text-xs sm:text-sm text-gray-600 dark:text-gray-400">
|
||
<span className="font-medium">Toplam:</span>
|
||
<span>{tasks.length} görev</span>
|
||
</div>
|
||
<button className="px-3 sm:px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center gap-2 transition-colors text-sm sm:text-base">
|
||
<HiPlus className="w-4 h-4 sm:w-5 sm:h-5" />
|
||
<span className="hidden sm:inline">Yeni Görev</span>
|
||
<span className="sm:hidden">Yeni</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Kanban Board */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3 lg:gap-4 overflow-x-auto pb-4">
|
||
|
||
<div className="kanban-container sm:contents">
|
||
{columns.map(column => {
|
||
const columnTasks = getTasksByStatus(column.id)
|
||
return (
|
||
<DroppableColumn key={column.id} id={column.id}>
|
||
<SortableContext
|
||
id={column.id}
|
||
items={columnTasks.map(t => t.id)}
|
||
strategy={verticalListSortingStrategy}
|
||
>
|
||
<div
|
||
className="kanban-column flex flex-col gap-2 sm:gap-3 bg-gray-100 dark:bg-gray-800/50 rounded-lg p-3 sm:p-4 min-h-[400px] sm:min-h-[500px] lg:min-h-[600px]"
|
||
data-status={column.id}
|
||
>
|
||
{/* Column Header */}
|
||
<div className="flex items-center justify-between mb-2">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-lg sm:text-xl">{column.icon}</span>
|
||
<h3 className="font-semibold text-gray-900 dark:text-white text-sm sm:text-base">
|
||
{column.title}
|
||
</h3>
|
||
</div>
|
||
<Badge content={columnTasks.length}></Badge>
|
||
</div>
|
||
|
||
{/* Tasks */}
|
||
<div className="space-y-3 flex-1">
|
||
{columnTasks.map(task => (
|
||
<SortableTaskCard
|
||
key={task.id}
|
||
task={task}
|
||
onTaskClick={setSelectedTask}
|
||
getPriorityColor={getPriorityColor}
|
||
getPriorityLabel={getPriorityLabel}
|
||
isOverdue={isOverdue}
|
||
/>
|
||
))}
|
||
</div>
|
||
|
||
{/* Add Task Button */}
|
||
<button
|
||
onClick={() => handleAddTask(column.id)}
|
||
className="w-full p-3 sm:p-4 border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg hover:border-blue-500 dark:hover:border-blue-500 hover:bg-blue-50 dark:hover:bg-blue-900/10 transition-colors text-gray-500 dark:text-gray-400 hover:text-blue-600 dark:hover:text-blue-400"
|
||
>
|
||
<HiPlus className="w-4 h-4 sm:w-5 sm:h-5 mx-auto" />
|
||
</button>
|
||
</div>
|
||
</SortableContext>
|
||
</DroppableColumn>
|
||
)
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* DragOverlay */}
|
||
<DragOverlay>
|
||
{activeId ? (
|
||
<div className="opacity-50">
|
||
{(() => {
|
||
const task = tasks.find(t => t.id === activeId)
|
||
if (!task) return null
|
||
return (
|
||
<div className="bg-white dark:bg-gray-800 rounded-lg p-4 border-2 border-blue-500 shadow-2xl">
|
||
<h4 className="font-semibold text-gray-900 dark:text-white">
|
||
{task.title}
|
||
</h4>
|
||
</div>
|
||
)
|
||
})()}
|
||
</div>
|
||
) : null}
|
||
</DragOverlay>
|
||
|
||
{/* New Task Modal */}
|
||
<AnimatePresence>
|
||
{showNewTaskModal && (
|
||
<>
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/50 z-40"
|
||
onClick={() => setShowNewTaskModal(false)}
|
||
/>
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-lg w-full"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||
<h2 className="text-lg sm:text-xl font-semibold text-gray-900 dark:text-white">
|
||
Yeni Görev Oluştur
|
||
</h2>
|
||
<button
|
||
onClick={() => setShowNewTaskModal(false)}
|
||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||
>
|
||
<HiXMark className="w-5 h-5 text-gray-500" />
|
||
</button>
|
||
</div>
|
||
|
||
<form
|
||
onSubmit={(e) => {
|
||
e.preventDefault()
|
||
const formData = new FormData(e.currentTarget)
|
||
const title = formData.get('title') as string
|
||
const description = formData.get('description') as string
|
||
if (title && description) {
|
||
handleCreateTask(title, description)
|
||
}
|
||
}}
|
||
className="p-4 sm:p-6 space-y-4"
|
||
>
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Görev Başlığı *
|
||
</label>
|
||
<input
|
||
type="text"
|
||
name="title"
|
||
required
|
||
placeholder="Görev başlığını yazın"
|
||
className="w-full px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||
Açıklama *
|
||
</label>
|
||
<textarea
|
||
name="description"
|
||
required
|
||
rows={4}
|
||
placeholder="Görev detaylarını yazın"
|
||
className="w-full px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 text-sm sm:text-base"
|
||
/>
|
||
</div>
|
||
|
||
<div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3">
|
||
<p className="text-xs sm:text-sm text-blue-700 dark:text-blue-300">
|
||
📋 Görev <strong>{columns.find(c => c.id === newTaskColumn)?.title}</strong> kolonuna eklenecek
|
||
</p>
|
||
</div>
|
||
|
||
<div className="flex gap-2 sm:gap-3 pt-4">
|
||
<button
|
||
type="button"
|
||
onClick={() => setShowNewTaskModal(false)}
|
||
className="flex-1 px-3 sm:px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm sm:text-base"
|
||
>
|
||
İptal
|
||
</button>
|
||
<button
|
||
type="submit"
|
||
className="flex-1 px-3 sm:px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors text-sm sm:text-base"
|
||
>
|
||
Oluştur
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</motion.div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
|
||
{/* Task Detail Modal */}
|
||
<AnimatePresence>
|
||
{selectedTask && (
|
||
<>
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
exit={{ opacity: 0 }}
|
||
className="fixed inset-0 bg-black/50 z-40"
|
||
onClick={() => setSelectedTask(null)}
|
||
/>
|
||
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-4">
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-3xl w-full max-h-[90vh] overflow-y-auto"
|
||
>
|
||
<div className="p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||
<span className={`px-2 sm:px-3 py-1 sm:py-1.5 text-xs sm:text-sm font-medium rounded border ${getPriorityColor(selectedTask.priority)}`}>
|
||
{getPriorityLabel(selectedTask.priority)}
|
||
</span>
|
||
<span className="px-2 sm:px-3 py-1 sm:py-1.5 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs sm:text-sm font-medium rounded">
|
||
📁 {selectedTask.project}
|
||
</span>
|
||
</div>
|
||
<button
|
||
onClick={() => setSelectedTask(null)}
|
||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
||
>
|
||
<HiXMark className="w-5 h-5 text-gray-500" />
|
||
</button>
|
||
</div>
|
||
<div className="p-4 sm:p-6 space-y-4 sm:space-y-6">
|
||
<div>
|
||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white mb-2">
|
||
{selectedTask.title}
|
||
</h2>
|
||
<p className="text-sm sm:text-base text-gray-700 dark:text-gray-300">
|
||
{selectedTask.description}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 sm:gap-6">
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||
Durum
|
||
</p>
|
||
<select
|
||
value={selectedTask.status}
|
||
onChange={(e) => handleStatusChange(selectedTask.id, e.target.value as TaskStatus)}
|
||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 cursor-pointer"
|
||
>
|
||
<option value="todo">📋 Yapılacak</option>
|
||
<option value="in-progress">⚙️ Devam Ediyor</option>
|
||
<option value="review">👀 İncelemede</option>
|
||
<option value="done">✅ Tamamlandı</option>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||
Son Tarih
|
||
</p>
|
||
<div className="flex items-center gap-2 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg">
|
||
<HiClock className="w-5 h-5 text-gray-400" />
|
||
<span className="text-gray-900 dark:text-white">
|
||
{dayjs(selectedTask.dueDate).format('DD MMMM YYYY')}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2 sm:mb-3">
|
||
Atananlar
|
||
</p>
|
||
<div className="flex flex-wrap gap-2 sm:gap-3">
|
||
{selectedTask.assignedTo.map((user, idx) => (
|
||
<div
|
||
key={idx}
|
||
className="flex items-center gap-2 px-2 sm:px-3 py-1.5 sm:py-2 bg-gray-100 dark:bg-gray-700 rounded-lg"
|
||
>
|
||
<img
|
||
src={user.avatar}
|
||
alt={user.fullName}
|
||
className="w-6 h-6 sm:w-8 sm:h-8 rounded-full"
|
||
/>
|
||
<span className="text-xs sm:text-sm text-gray-900 dark:text-white">
|
||
{user.fullName}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{selectedTask.labels.length > 0 && (
|
||
<div>
|
||
<p className="text-sm font-medium text-gray-600 dark:text-gray-400 mb-2">
|
||
Etiketler
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{selectedTask.labels.map((label, idx) => (
|
||
<span
|
||
key={idx}
|
||
className="px-2 sm:px-3 py-1 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-xs sm:text-sm rounded"
|
||
>
|
||
{label}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="pt-6 border-t border-gray-200 dark:border-gray-700 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
||
Oluşturan: {selectedTask.assignedBy.fullName} • {dayjs(selectedTask.createdAt).format('DD MMMM YYYY')}
|
||
</p>
|
||
<button
|
||
onClick={() => handleDeleteTask(selectedTask.id)}
|
||
className="px-3 sm:px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center gap-2 transition-colors text-sm"
|
||
>
|
||
<HiTrash className="w-4 h-4" />
|
||
Görevi Sil
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
</div>
|
||
</>
|
||
)}
|
||
</AnimatePresence>
|
||
</div>
|
||
</div>
|
||
</DndContext>
|
||
)
|
||
}
|
||
|
||
export default TasksModule
|