refactor: UUID-based member refs in API types and components

- Task interface: assignee_id/assignee, reviewer_id/reviewer, watcher_ids
- Message interface: author_id/author вместо author_slug
- API functions: assignTask использует assignee_id
- KanbanBoard: обновлен под новые поля задач
- TaskModal: assignee selection по ID вместо slug
- ChatPanel: отображение author через relationship
This commit is contained in:
Markov 2026-02-25 00:13:36 +01:00
parent 8df4179dd7
commit c7144328fa
4 changed files with 18 additions and 15 deletions

View File

@ -125,7 +125,7 @@ export default function ChatPanel({ chatId }: Props) {
<div key={msg.id} className="text-sm" style={bgStyle}> <div key={msg.id} className="text-sm" style={bgStyle}>
<div className="flex items-center gap-1 text-xs text-[var(--muted)] mb-0.5"> <div className="flex items-center gap-1 text-xs text-[var(--muted)] mb-0.5">
<span>{AUTHOR_ICON[msg.author_type] || "👤"}</span> <span>{AUTHOR_ICON[msg.author_type] || "👤"}</span>
<span className="font-medium">{msg.author_slug}</span> <span className="font-medium">{msg.author?.name || msg.author?.slug || "Unknown"}</span>
<span>·</span> <span>·</span>
<span>{new Date(msg.created_at).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit" })}</span> <span>{new Date(msg.created_at).toLocaleString("ru-RU", { hour: "2-digit", minute: "2-digit" })}</span>
</div> </div>

View File

@ -63,7 +63,7 @@ export default function KanbanBoard({ projectId, projectSlug }: Props) {
const unsubscribeAssigned = wsClient.on("task.assigned", (data: any) => { const unsubscribeAssigned = wsClient.on("task.assigned", (data: any) => {
if (data.project_id === projectId) { if (data.project_id === projectId) {
setTasks((prev) => prev.map((t) => t.id === data.id ? { ...t, assignee_slug: data.assignee_slug, assigned_at: data.assigned_at } : t)); setTasks((prev) => prev.map((t) => t.id === data.id ? { ...t, assignee_id: data.assignee_id, assignee: data.assignee, assigned_at: data.assigned_at } : t));
} }
}); });
@ -106,8 +106,8 @@ export default function KanbanBoard({ projectId, projectSlug }: Props) {
style={{ background: PRIORITY_COLORS[task.priority] || "#737373" }} style={{ background: PRIORITY_COLORS[task.priority] || "#737373" }}
title={task.priority} /> title={task.priority} />
<span className="text-xs text-[var(--muted)]">{prefix}-{task.number}</span> <span className="text-xs text-[var(--muted)]">{prefix}-{task.number}</span>
{task.assignee_slug && ( {task.assignee && (
<span className="text-xs text-[var(--muted)] ml-auto"> {task.assignee_slug}</span> <span className="text-xs text-[var(--muted)] ml-auto"> {task.assignee.slug}</span>
)} )}
</div> </div>
<div className="text-sm ml-3.5">{task.title}</div> <div className="text-sm ml-3.5">{task.title}</div>

View File

@ -42,7 +42,7 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
const [description, setDescription] = useState(task.description || ""); const [description, setDescription] = useState(task.description || "");
const [status, setStatus] = useState(task.status); const [status, setStatus] = useState(task.status);
const [priority, setPriority] = useState(task.priority); const [priority, setPriority] = useState(task.priority);
const [assigneeSlug, setAssigneeSlug] = useState(task.assignee_slug || ""); const [assigneeId, setAssigneeId] = useState(task.assignee_id || "");
const [members, setMembers] = useState<Member[]>([]); const [members, setMembers] = useState<Member[]>([]);
const [steps, setSteps] = useState<Step[]>(task.steps || []); const [steps, setSteps] = useState<Step[]>(task.steps || []);
const [comments, setComments] = useState<Message[]>([]); const [comments, setComments] = useState<Message[]>([]);
@ -307,16 +307,16 @@ export default function TaskModal({ task, projectId: _projectId, projectSlug: _p
<div> <div>
<div className="text-xs text-[var(--muted)] mb-1">Исполнитель</div> <div className="text-xs text-[var(--muted)] mb-1">Исполнитель</div>
<select <select
value={assigneeSlug} value={assigneeId}
onChange={(e) => { onChange={(e) => {
setAssigneeSlug(e.target.value); setAssigneeId(e.target.value);
save({ assignee_slug: e.target.value || undefined } as any); save({ assignee_id: e.target.value || undefined } as any);
}} }}
className="w-full bg-[var(--bg)] border border-[var(--border)] rounded px-2 py-1.5 text-sm outline-none focus:border-[var(--accent)]" className="w-full bg-[var(--bg)] border border-[var(--border)] rounded px-2 py-1.5 text-sm outline-none focus:border-[var(--accent)]"
> >
<option value="">Не назначен</option> <option value="">Не назначен</option>
{members.map((m) => ( {members.map((m) => (
<option key={m.slug} value={m.slug}> <option key={m.id} value={m.id}>
{m.type === "agent" ? "🤖 " : "👤 "}{m.name} {m.type === "agent" ? "🤖 " : "👤 "}{m.name}
</option> </option>
))} ))}

View File

@ -106,9 +106,11 @@ export interface Task {
status: string; status: string;
priority: string; priority: string;
labels: string[]; labels: string[];
assignee_slug: string | null; assignee_id: string | null;
reviewer_slug: string | null; assignee: Member | null;
watchers: string[]; reviewer_id: string | null;
reviewer: Member | null;
watcher_ids: string[];
depends_on: string[]; depends_on: string[];
position: number; position: number;
time_spent: number; time_spent: number;
@ -128,7 +130,8 @@ export interface Message {
task_id: string | null; task_id: string | null;
parent_id: string | null; parent_id: string | null;
author_type: string; author_type: string;
author_slug: string; author_id: string;
author: Member | null;
content: string; content: string;
mentions: string[]; mentions: string[];
voice_url: string | null; voice_url: string | null;
@ -203,10 +206,10 @@ export async function rejectTask(taskId: string, reason: string): Promise<{ok: b
}); });
} }
export async function assignTask(taskId: string, assigneeSlug: string): Promise<Task> { export async function assignTask(taskId: string, assigneeId: string): Promise<Task> {
return request(`/api/v1/tasks/${taskId}/assign`, { return request(`/api/v1/tasks/${taskId}/assign`, {
method: "POST", method: "POST",
body: JSON.stringify({ assignee_slug: assigneeSlug }) body: JSON.stringify({ assignee_id: assigneeId })
}); });
} }