From 9ebddce01fc01143d46e5af16e79b88feb149b3b Mon Sep 17 00:00:00 2001 From: Markov Date: Sun, 22 Feb 2026 19:03:24 +0100 Subject: [PATCH] feat: task key prefix (TE-1), create task modal, chat via REST - Task keys: PROJECT_SLUG[:2]-NUMBER (e.g. TE-1) - Create task: proper modal with title, description, priority - Chat: sends via REST API (not WS), uses project chat_id - Assignee shown on task cards --- src/app/(protected)/projects/[slug]/page.tsx | 2 +- src/components/ChatPanel.tsx | 41 ++-- src/components/CreateTaskModal.tsx | 112 ++++++++++ src/components/KanbanBoard.tsx | 224 ++++++------------- src/components/TaskModal.tsx | 5 +- src/lib/api.ts | 1 + 6 files changed, 215 insertions(+), 170 deletions(-) create mode 100644 src/components/CreateTaskModal.tsx diff --git a/src/app/(protected)/projects/[slug]/page.tsx b/src/app/(protected)/projects/[slug]/page.tsx index e8a0175..585cb46 100644 --- a/src/app/(protected)/projects/[slug]/page.tsx +++ b/src/app/(protected)/projects/[slug]/page.tsx @@ -41,7 +41,7 @@ export default function ProjectPage() { )} - + {project.chat_id && }
diff --git a/src/components/ChatPanel.tsx b/src/components/ChatPanel.tsx index 5ecf1f2..a11ea9d 100644 --- a/src/components/ChatPanel.tsx +++ b/src/components/ChatPanel.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { Message, getMessages } from "@/lib/api"; +import { Message, getMessages, sendMessage } from "@/lib/api"; import { wsClient } from "@/lib/ws"; const AUTHOR_ICON: Record = { @@ -11,27 +11,23 @@ const AUTHOR_ICON: Record = { }; interface Props { - projectId: string; + chatId: string; } -export default function ChatPanel({ projectId }: Props) { +export default function ChatPanel({ chatId }: Props) { const [messages, setMessages] = useState([]); const [input, setInput] = useState(""); - const [chatId, setChatId] = useState(null); const [open, setOpen] = useState(false); + const [sending, setSending] = useState(false); const bottomRef = useRef(null); - // Find project chat ID from WS auth data or use lobby useEffect(() => { - // For now use lobby - const id = wsClient.lobbyId; - if (id) { - setChatId(id); - getMessages({ chat_id: id, limit: 50 }).then(setMessages).catch(() => {}); + if (chatId) { + getMessages({ chat_id: chatId, limit: 50 }).then(setMessages).catch(() => {}); } - }, []); + }, [chatId]); - // Listen for new messages + // Listen for new messages via WS useEffect(() => { const unsub = wsClient.on("message.new", (data: any) => { if (data.chat_id === chatId) { @@ -45,10 +41,20 @@ export default function ChatPanel({ projectId }: Props) { bottomRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); - const handleSend = () => { - if (!input.trim() || !chatId) return; - wsClient.sendChat(chatId, input.trim()); + const handleSend = async () => { + if (!input.trim() || !chatId || sending) return; + const text = input.trim(); setInput(""); + setSending(true); + try { + const msg = await sendMessage({ chat_id: chatId, content: text }); + setMessages((prev) => [...prev, msg]); + } catch (e) { + console.error("Failed to send:", e); + setInput(text); // restore on error + } finally { + setSending(false); + } }; return ( @@ -71,6 +77,9 @@ export default function ChatPanel({ projectId }: Props) { {msg.content} ))} + {messages.length === 0 && ( +
Нет сообщений
+ )}
@@ -81,7 +90,7 @@ export default function ChatPanel({ projectId }: Props) { placeholder="Сообщение..." className="flex-1 bg-[var(--bg)] border border-[var(--border)] rounded px-3 py-1.5 text-sm outline-none focus:border-[var(--accent)]" /> - +
)} diff --git a/src/components/CreateTaskModal.tsx b/src/components/CreateTaskModal.tsx new file mode 100644 index 0000000..98be64a --- /dev/null +++ b/src/components/CreateTaskModal.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { useState } from "react"; +import { createTask, Task } from "@/lib/api"; + +const PRIORITIES = [ + { key: "low", label: "Low", color: "#737373" }, + { key: "medium", label: "Medium", color: "#3b82f6" }, + { key: "high", label: "High", color: "#f59e0b" }, + { key: "critical", label: "Critical", color: "#ef4444" }, +]; + +interface Props { + projectSlug: string; + initialStatus: string; + onClose: () => void; + onCreated: (task: Task) => void; +} + +export default function CreateTaskModal({ projectSlug, initialStatus, onClose, onCreated }: Props) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [priority, setPriority] = useState("medium"); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(""); + + const handleSubmit = async () => { + if (!title.trim()) return; + setSaving(true); + setError(""); + try { + const task = await createTask(projectSlug, { + title: title.trim(), + description: description.trim() || undefined, + status: initialStatus, + priority, + }); + onCreated(task); + onClose(); + } catch (e: any) { + setError(e.message || "Ошибка"); + } finally { + setSaving(false); + } + }; + + return ( +
+
e.stopPropagation()} + > +

Новая задача

+ +
+
+ + setTitle(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSubmit()} + placeholder="Название задачи..." + className="w-full bg-[var(--bg)] border border-[var(--border)] rounded-lg px-3 py-2 text-sm outline-none focus:border-[var(--accent)]" + /> +
+ +
+ +