From b11ec711c5cb9b4790e0a9031822670fb6a30da8 Mon Sep 17 00:00:00 2001 From: Oleg Date: Wed, 19 Aug 2026 08:09:14 +0000 Subject: [PATCH] Dashboard tag filter now covers every tag, not just ones already in use; add a default tag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard's tag filter chips were derived from tags already attached to a currently-loaded ticket — a brand-new tag (e.g. VIP was the only one ever used) had nothing to filter by until someone happened to apply it. The filter bar now lists every tag defined in Settings, fetched server-side alongside the ticket list. Also added a "default tag" concept: mark one tag as default in Settings (star icon), and it's auto-attached to every new ticket the moment it's created, across every channel (email/telegram/widget/manual) — matches the request to have "В порядке очереди" applied by default. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01GteWhnWKTmnXcsd5jx6H7u --- src/app/(admin)/dashboard/page.tsx | 6 +- src/app/(admin)/dashboard/ticket-board.tsx | 16 +- .../(admin)/settings/tags/tags-manager.tsx | 25 +- src/app/api/tags/[id]/route.ts | 19 +- .../db/migrations/0008_quick_ultragirl.sql | 1 + src/lib/db/migrations/meta/0008_snapshot.json | 1050 +++++++++++++++++ src/lib/db/migrations/meta/_journal.json | 7 + src/lib/db/schema.ts | 4 + src/lib/tickets/service.ts | 25 +- src/lib/tickets/types.ts | 1 + 10 files changed, 1139 insertions(+), 15 deletions(-) create mode 100644 src/lib/db/migrations/0008_quick_ultragirl.sql create mode 100644 src/lib/db/migrations/meta/0008_snapshot.json diff --git a/src/app/(admin)/dashboard/page.tsx b/src/app/(admin)/dashboard/page.tsx index 40ad72d..2f255d0 100644 --- a/src/app/(admin)/dashboard/page.tsx +++ b/src/app/(admin)/dashboard/page.tsx @@ -1,6 +1,6 @@ import { redirect } from "next/navigation"; import { getCurrentSession } from "@/lib/auth/session"; -import { listTickets } from "@/lib/tickets/service"; +import { listTickets, listTags } from "@/lib/tickets/service"; import { TicketBoard } from "./ticket-board"; export default async function DashboardPage() { @@ -8,6 +8,6 @@ export default async function DashboardPage() { if (!session) redirect("/login"); const currentUser = { id: session.user.id, role: session.user.role }; - const tickets = await listTickets(undefined, currentUser); - return ; + const [tickets, tags] = await Promise.all([listTickets(undefined, currentUser), listTags()]); + return ; } diff --git a/src/app/(admin)/dashboard/ticket-board.tsx b/src/app/(admin)/dashboard/ticket-board.tsx index 1f3de5b..006e5c1 100644 --- a/src/app/(admin)/dashboard/ticket-board.tsx +++ b/src/app/(admin)/dashboard/ticket-board.tsx @@ -6,7 +6,7 @@ import { AnimatePresence, motion } from "framer-motion"; import { Send, Globe, UserRound, Mail, MessageSquare, Plus, Inbox, Search, X } from "lucide-react"; import { useTicketEvents } from "@/lib/events/use-ticket-events"; import { formatRelativeTime } from "@/lib/format"; -import type { TicketDTO, TicketStatus } from "@/lib/tickets/types"; +import type { TicketDTO, TicketStatus, TagDTO } from "@/lib/tickets/types"; import { isTicketVisibleTo } from "@/lib/tickets/visibility"; import { NewTicketModal } from "./new-ticket-modal"; import { Avatar } from "@/components/avatar"; @@ -45,9 +45,11 @@ const PRIORITY_COLOR: Record = { export function TicketBoard({ initialTickets, + allTags, currentUser, }: { initialTickets: TicketDTO[]; + allTags: TagDTO[]; currentUser: { id: string; role: "admin" | "agent" }; }) { const [tickets, setTickets] = useState(initialTickets); @@ -100,13 +102,11 @@ export function TicketBoard({ const baseTickets = query.trim() ? (searchResults ?? tickets) : tickets; - const availableTags = useMemo(() => { - const map = new Map(); - for (const ticket of baseTickets) { - for (const tag of ticket.tags) map.set(tag.id, tag); - } - return [...map.values()].sort((a, b) => a.name.localeCompare(b.name)); - }, [baseTickets]); + // Every tag that exists, not just ones already applied to a currently + // loaded ticket — otherwise a freshly created tag has nothing to filter + // by until someone happens to tag a ticket with it. allTags is already + // name-sorted server-side (listTags orders by tagsTable.name). + const availableTags = allTags; const filteredTickets = useMemo(() => { if (!activeTagId) return baseTickets; diff --git a/src/app/(admin)/settings/tags/tags-manager.tsx b/src/app/(admin)/settings/tags/tags-manager.tsx index f5a1aaf..b8136a4 100644 --- a/src/app/(admin)/settings/tags/tags-manager.tsx +++ b/src/app/(admin)/settings/tags/tags-manager.tsx @@ -1,7 +1,7 @@ "use client"; import { useState } from "react"; -import { Plus, Trash2 } from "lucide-react"; +import { Plus, Star, Trash2 } from "lucide-react"; import { TAG_COLOR_CLASSES, TAG_COLORS, TAG_COLOR_LABELS } from "@/lib/tags/colors"; import type { TagDTO, TagColor } from "@/lib/tickets/types"; @@ -40,6 +40,18 @@ export function TagsManager({ initialTags }: { initialTags: TagDTO[] }) { setTags((prev) => prev.filter((t) => t.id !== id)); } + async function handleToggleDefault(tag: TagDTO) { + const nextIsDefault = !tag.isDefault; + const res = await fetch(`/api/tags/${tag.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ isDefault: nextIsDefault }), + }); + if (!res.ok) return; + const { tags: updated } = await res.json(); + setTags(updated); + } + return (
@@ -77,12 +89,23 @@ export function TagsManager({ initialTags }: { initialTags: TagDTO[] }) { {tags.length === 0 &&

Тегов пока нет.

} +

+ Тег по умолчанию отмечается звёздочкой — он автоматически ставится на каждую новую заявку. +

+
{tags.map((tag) => ( + {tag.name}