From 671195fb7da4185440451e69af407c42cef558e2 Mon Sep 17 00:00:00 2001 From: kovakmedya Date: Thu, 30 Apr 2026 05:57:35 +0300 Subject: [PATCH] feat(tasks): Kanban board with drag-and-drop (dnd-kit) Replaces the template's /tasks demo (deleted) with a real multi-tenant Kanban board. Schema/validation: - lib/validation/tasks.ts (taskSchema with status/priority enums + dueDate optional + assignee/customer optional) - lib/appwrite/task-actions.ts: createTaskAction, updateTaskAction, deleteTaskAction, moveTaskAction (used by drag-drop). All audit-logged; moveTaskAction only audits when status actually changes. - lib/appwrite/task-queries.ts: listTasks ordered by 'order' asc. UI: - /tasks server page assembles { tasks, customers, teamMembers } and passes to TasksBoard. Removed the template's data-table demo files. - TasksBoard (client): 4 droppable columns. Columns use @dnd-kit/core useDroppable; cards inside each column are SortableContext+useSortable for intra-column ordering. closestCorners collision detection. - Drag-end computes new 'order' as midpoint between adjacent tasks (no full reindex), updates UI optimistically, then persists via moveTaskAction. Rolls back on server error with toast. - TaskCard: priority badge (color-coded), due-date badge (red if overdue), assignee badge, customer subtitle, dropdown (Edit/Delete) on hover. - TaskFormSheet: title/description/status/priority/dueDate/assignee/ customer. Uses sentinel '__none__' for nullable Selects (Radix Select forbids empty string values), stripped before submit. - DragOverlay shows the dragged card rotated 3deg with shadow. --- .../tasks/components/add-task-modal.tsx | 256 ------------- .../(dashboard)/tasks/components/columns.tsx | 160 -------- .../components/data-table-column-header.tsx | 43 --- .../components/data-table-faceted-filter.tsx | 155 -------- .../components/data-table-pagination.tsx | 99 ----- .../components/data-table-row-actions.tsx | 52 --- .../tasks/components/data-table-toolbar.tsx | 174 --------- .../components/data-table-view-options.tsx | 59 --- .../tasks/components/data-table.tsx | 129 ------- .../tasks/components/task-card.tsx | 124 ++++++ .../tasks/components/task-form-sheet.tsx | 222 +++++++++++ .../tasks/components/tasks-board.tsx | 310 +++++++++++++++ src/app/(dashboard)/tasks/components/types.ts | 40 ++ .../(dashboard)/tasks/components/user-nav.tsx | 64 ---- src/app/(dashboard)/tasks/data/data.tsx | 71 ---- src/app/(dashboard)/tasks/data/schema.ts | 13 - src/app/(dashboard)/tasks/data/tasks.json | 352 ------------------ src/app/(dashboard)/tasks/page.tsx | 224 +++-------- src/lib/appwrite/task-actions.ts | 244 ++++++++++++ src/lib/appwrite/task-queries.ts | 24 ++ src/lib/appwrite/task-types.ts | 7 + src/lib/validation/tasks.ts | 18 + 22 files changed, 1047 insertions(+), 1793 deletions(-) delete mode 100644 src/app/(dashboard)/tasks/components/add-task-modal.tsx delete mode 100644 src/app/(dashboard)/tasks/components/columns.tsx delete mode 100644 src/app/(dashboard)/tasks/components/data-table-column-header.tsx delete mode 100644 src/app/(dashboard)/tasks/components/data-table-faceted-filter.tsx delete mode 100644 src/app/(dashboard)/tasks/components/data-table-pagination.tsx delete mode 100644 src/app/(dashboard)/tasks/components/data-table-row-actions.tsx delete mode 100644 src/app/(dashboard)/tasks/components/data-table-toolbar.tsx delete mode 100644 src/app/(dashboard)/tasks/components/data-table-view-options.tsx delete mode 100644 src/app/(dashboard)/tasks/components/data-table.tsx create mode 100644 src/app/(dashboard)/tasks/components/task-card.tsx create mode 100644 src/app/(dashboard)/tasks/components/task-form-sheet.tsx create mode 100644 src/app/(dashboard)/tasks/components/tasks-board.tsx create mode 100644 src/app/(dashboard)/tasks/components/types.ts delete mode 100644 src/app/(dashboard)/tasks/components/user-nav.tsx delete mode 100644 src/app/(dashboard)/tasks/data/data.tsx delete mode 100644 src/app/(dashboard)/tasks/data/schema.ts delete mode 100644 src/app/(dashboard)/tasks/data/tasks.json create mode 100644 src/lib/appwrite/task-actions.ts create mode 100644 src/lib/appwrite/task-queries.ts create mode 100644 src/lib/appwrite/task-types.ts create mode 100644 src/lib/validation/tasks.ts diff --git a/src/app/(dashboard)/tasks/components/add-task-modal.tsx b/src/app/(dashboard)/tasks/components/add-task-modal.tsx deleted file mode 100644 index 940b4f5..0000000 --- a/src/app/(dashboard)/tasks/components/add-task-modal.tsx +++ /dev/null @@ -1,256 +0,0 @@ -"use client" - -import { useState } from "react" -import { Plus } from "lucide-react" -import { z } from "zod" - -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" -import { Label } from "@/components/ui/label" -import { Textarea } from "@/components/ui/textarea" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" - -import { priorities, statuses, categories } from "../data/data" -import type { Task } from "../data/schema" - -// Extended task schema for the form -const taskFormSchema = z.object({ - id: z.string(), - title: z.string().min(1, "Title is required"), - description: z.string().optional(), - status: z.string(), - category: z.string(), - priority: z.string(), -}) - -type TaskFormData = z.infer - -interface AddTaskModalProps { - onAddTask?: (task: Task) => void - trigger?: React.ReactNode -} - -export function AddTaskModal({ onAddTask, trigger }: AddTaskModalProps) { - const [open, setOpen] = useState(false) - const [formData, setFormData] = useState({ - id: "", - title: "", - description: "", - status: "todo", - category: "feature", - priority: "normal", - }) - const [errors, setErrors] = useState>({}) - - // Generate unique task ID - const generateTaskId = () => { - const prefix = "TASK" - const number = Math.floor(Math.random() * 9999) + 1000 - return `${prefix}-${number}` - } - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault() - - try { - // Validate form data - const validatedData = taskFormSchema.parse({ - ...formData, - id: generateTaskId(), - }) - - // Create the task - const newTask: Task = { - id: validatedData.id, - title: validatedData.title, - status: validatedData.status, - category: validatedData.category, - priority: validatedData.priority, - } - - onAddTask?.(newTask) - - // Reset form and close modal - setFormData({ - id: "", - title: "", - description: "", - status: "todo", - category: "feature", - priority: "normal", - }) - setErrors({}) - setOpen(false) - } catch (error) { - if (error instanceof z.ZodError) { - const newErrors: Record = {} - error.issues.forEach((issue) => { - if (issue.path[0]) { - newErrors[issue.path[0] as string] = issue.message - } - }) - setErrors(newErrors) - } - } - } - - const handleCancel = () => { - setFormData({ - id: "", - title: "", - description: "", - status: "todo", - category: "feature", - priority: "normal", - }) - setErrors({}) - setOpen(false) - } - - return ( - - - {trigger || ( - - )} - - - - Add New Task - - Create a new task to track work and progress. Fill in the details below. - - - -
- {/* Task Title */} -
- - setFormData(prev => ({ ...prev, title: e.target.value }))} - className={errors.title ? "border-red-500" : ""} - /> - {errors.title && ( -

{errors.title}

- )} -
- - {/* Task Description */} -
- -