erp-platform/ui/src/components/intranet/Tasks/index.tsx
2025-10-19 01:37:20 +03:00

655 lines
26 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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">
ı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