feat(kanban): READ – hämta och rendera projekt‑tasks i tre kolumner
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
34
src/features/tasks/api.ts
Normal file
34
src/features/tasks/api.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { api } from '@/lib/http'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import type { Task, TaskStatus } from '@/types/task'
|
||||||
|
|
||||||
|
export function projectTasksKey(projectId: string, params?: Record<string, string | number | undefined>) {
|
||||||
|
return ['projectTasks', projectId, params] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchProjectTasks(projectId: string, params?: { status?: TaskStatus }) {
|
||||||
|
const search = new URLSearchParams()
|
||||||
|
if (params?.status) search.set('status', params.status)
|
||||||
|
|
||||||
|
const url = `api/v1/projects/${projectId}/tasks${search.size ? `?${search.toString()}` : ''}`
|
||||||
|
|
||||||
|
// Läs råsvaret och normalisera till Task[]
|
||||||
|
const raw = await api.get(url).json<any>()
|
||||||
|
|
||||||
|
// Stöd både ren lista och Page<T>
|
||||||
|
const list: unknown =
|
||||||
|
Array.isArray(raw) ? raw
|
||||||
|
: Array.isArray(raw?.content) ? raw.content
|
||||||
|
: Array.isArray(raw?.items) ? raw.items
|
||||||
|
: []
|
||||||
|
|
||||||
|
return list as Task[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProjectTasksQuery(projectId: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: projectTasksKey(projectId),
|
||||||
|
queryFn: () => fetchProjectTasks(projectId),
|
||||||
|
staleTime: 10_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
20
src/features/tasks/components/KanbanColumn.tsx
Normal file
20
src/features/tasks/components/KanbanColumn.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import type { Task, TaskStatus } from '@/types/task'
|
||||||
|
import { TaskCard } from './TaskCard'
|
||||||
|
|
||||||
|
export function KanbanColumn({ title, status, items }: { title: string; status: TaskStatus; items: Task[] }) {
|
||||||
|
return (
|
||||||
|
<section aria-labelledby={`col-${status}`} className="flex-1 min-w-[260px]">
|
||||||
|
<header className="mb-2 flex items-center justify-between">
|
||||||
|
<h3 id={`col-${status}`} className="text-sm font-semibold uppercase tracking-wide opacity-70">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs opacity-60">{items.length}</span>
|
||||||
|
</header>
|
||||||
|
<div role="list" aria-describedby={`col-${status}`} className="space-y-2">
|
||||||
|
{items.map((t) => (
|
||||||
|
<TaskCard key={t.id} task={t} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
24
src/features/tasks/components/TaskCard.tsx
Normal file
24
src/features/tasks/components/TaskCard.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { Task } from '@/types/task'
|
||||||
|
|
||||||
|
export function TaskCard({ task }: { task: Task }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="listitem"
|
||||||
|
tabIndex={0}
|
||||||
|
className="rounded-xl border bg-white px-3 py-2 shadow-sm outline-offset-2 focus:outline focus:outline-2 focus:outline-indigo-500"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<h4 className="text-sm font-medium leading-5">{task.title}</h4>
|
||||||
|
{task.priority && (
|
||||||
|
<span className="rounded-full border px-2 text-xs opacity-80">{task.priority}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{task.dueDate && (
|
||||||
|
<div className="mt-1 text-xs opacity-70">Due: {new Date(task.dueDate).toLocaleDateString()}</div>
|
||||||
|
)}
|
||||||
|
{task.assigneeName && (
|
||||||
|
<div className="mt-1 text-xs opacity-70">Assignee: {task.assigneeName}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,2 +1,32 @@
|
|||||||
// src/pages/ProjectBoardPage.tsx
|
import { useParams } from 'react-router-dom'
|
||||||
export function ProjectBoardPage() { return <div>Project Kanban</div> }
|
import { useProjectTasksQuery } from '@/features/tasks/api'
|
||||||
|
import type { Task } from '@/types/task'
|
||||||
|
import { KanbanColumn } from '@/features/tasks/components/KanbanColumn'
|
||||||
|
|
||||||
|
function splitByStatus(items: Task[] | unknown) {
|
||||||
|
const arr = Array.isArray(items) ? items as Task[] : []
|
||||||
|
return {
|
||||||
|
OPEN: arr.filter(t => t.status === 'OPEN'),
|
||||||
|
IN_PROGRESS: arr.filter(t => t.status === 'IN_PROGRESS'),
|
||||||
|
DONE: arr.filter(t => t.status === 'DONE'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectBoardPage() {
|
||||||
|
const { projectId = '' } = useParams()
|
||||||
|
const { data, isLoading, isError } = useProjectTasksQuery(projectId)
|
||||||
|
|
||||||
|
if (isLoading) return <p>Laddar tasks…</p>
|
||||||
|
if (isError) return <p>Något gick fel när tasks skulle hämtas.</p>
|
||||||
|
|
||||||
|
const tasks = data ?? []
|
||||||
|
const cols = splitByStatus(tasks)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<KanbanColumn title="Open" status="OPEN" items={cols.OPEN ?? []} />
|
||||||
|
<KanbanColumn title="In progress" status="IN_PROGRESS" items={cols.IN_PROGRESS} />
|
||||||
|
<KanbanColumn title="Done" status="DONE" items={cols.DONE} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
src/types/task.ts
Normal file
12
src/types/task.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
export type TaskStatus = 'OPEN' | 'IN_PROGRESS' | 'DONE'
|
||||||
|
export type TaskPriority = 'LOW' | 'MEDIUM' | 'HIGH'
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
description?: string | null
|
||||||
|
status: TaskStatus
|
||||||
|
priority?: TaskPriority | null
|
||||||
|
dueDate?: string | null // ISO
|
||||||
|
assigneeName?: string | null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user