656 lines
26 KiB
TypeScript
656 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
|