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.
This commit is contained in:
kovakmedya
2026-04-30 05:57:35 +03:00
parent add2317717
commit 671195fb7d
22 changed files with 1047 additions and 1793 deletions
+18
View File
@@ -0,0 +1,18 @@
import { z } from "zod";
export const taskSchema = z.object({
title: z.string().trim().min(1, "Başlık zorunlu.").max(255),
description: z
.string()
.trim()
.max(5000)
.optional()
.transform((v) => (v ? v : undefined)),
status: z.enum(["backlog", "todo", "in_progress", "done"]).optional().default("todo"),
priority: z.enum(["low", "medium", "high", "urgent"]).optional().default("medium"),
dueDate: z.string().optional().transform((v) => (v ? v : undefined)),
assigneeId: z.string().optional().transform((v) => (v ? v : undefined)),
customerId: z.string().optional().transform((v) => (v ? v : undefined)),
});
export type TaskInput = z.infer<typeof taskSchema>;