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) => ( + handleToggleDefault(tag)} + className="opacity-60 hover:opacity-100" + title={tag.isDefault ? "Убрать как тег по умолчанию" : "Сделать тегом по умолчанию"} + > + + {tag.name} handleDelete(tag.id)} className="opacity-60 hover:opacity-100"> diff --git a/src/app/api/tags/[id]/route.ts b/src/app/api/tags/[id]/route.ts index 807ca0b..f25919c 100644 --- a/src/app/api/tags/[id]/route.ts +++ b/src/app/api/tags/[id]/route.ts @@ -1,8 +1,25 @@ export const runtime = "nodejs"; import { NextResponse } from "next/server"; +import { z } from "zod"; import { requireSession } from "@/lib/auth/require"; -import { deleteTag } from "@/lib/tickets/service"; +import { deleteTag, setDefaultTag } from "@/lib/tickets/service"; + +const patchSchema = z.object({ isDefault: z.boolean() }); + +export async function PATCH(request: Request, { params }: { params: Promise<{ id: string }> }) { + const { session, response } = await requireSession(); + if (!session) return response; + + const { id } = await params; + const parsed = patchSchema.safeParse(await request.json().catch(() => null)); + if (!parsed.success) { + return NextResponse.json({ error: "Invalid input" }, { status: 400 }); + } + + const tags = await setDefaultTag(parsed.data.isDefault ? id : null); + return NextResponse.json({ tags }); +} export async function DELETE(_request: Request, { params }: { params: Promise<{ id: string }> }) { const { session, response } = await requireSession(); diff --git a/src/lib/db/migrations/0008_quick_ultragirl.sql b/src/lib/db/migrations/0008_quick_ultragirl.sql new file mode 100644 index 0000000..be695ee --- /dev/null +++ b/src/lib/db/migrations/0008_quick_ultragirl.sql @@ -0,0 +1 @@ +ALTER TABLE `tags` ADD `is_default` integer DEFAULT false NOT NULL; \ No newline at end of file diff --git a/src/lib/db/migrations/meta/0008_snapshot.json b/src/lib/db/migrations/meta/0008_snapshot.json new file mode 100644 index 0000000..9d0b30c --- /dev/null +++ b/src/lib/db/migrations/meta/0008_snapshot.json @@ -0,0 +1,1050 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "8c9be6b6-e2ab-486c-bc14-3ab2d96a7525", + "prevId": "f3f7178d-b3ff-4333-af59-11273f1c5d36", + "tables": { + "attachments": { + "name": "attachments", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_bytes": { + "name": "size_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "storage_key": { + "name": "storage_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_id": { + "name": "content_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "attachments_message_id_messages_id_fk": { + "name": "attachments_message_id_messages_id_fk", + "tableFrom": "attachments", + "tableTo": "messages", + "columnsFrom": [ + "message_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "canned_responses": { + "name": "canned_responses", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "customers": { + "name": "customers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "telegram_chat_id": { + "name": "telegram_chat_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "portal_token": { + "name": "portal_token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "customers_telegram_chat_id_unique": { + "name": "customers_telegram_chat_id_unique", + "columns": [ + "telegram_chat_id" + ], + "isUnique": true + }, + "customers_portal_token_unique": { + "name": "customers_portal_token_unique", + "columns": [ + "portal_token" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ldap_config": { + "name": "ldap_config", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "host": { + "name": "host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 389 + }, + "use_tls": { + "name": "use_tls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "bind_dn": { + "name": "bind_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bind_password_enc": { + "name": "bind_password_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "base_dn": { + "name": "base_dn", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_filter": { + "name": "user_filter", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'(mail={{email}})'" + }, + "list_filter": { + "name": "list_filter", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'(objectClass=person)'" + }, + "default_role": { + "name": "default_role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'agent'" + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "mailbox_config": { + "name": "mailbox_config", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "imap_host": { + "name": "imap_host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imap_port": { + "name": "imap_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 993 + }, + "smtp_host": { + "name": "smtp_host", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "smtp_port": { + "name": "smtp_port", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 587 + }, + "user": { + "name": "user", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_enc": { + "name": "password_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allow_insecure_tls": { + "name": "allow_insecure_tls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_type": { + "name": "author_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "author_name": { + "name": "author_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "body_html": { + "name": "body_html", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'public'" + }, + "email_message_id": { + "name": "email_message_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_ticket_id_tickets_id_fk": { + "name": "messages_ticket_id_tickets_id_fk", + "tableFrom": "messages", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "push_subscriptions": { + "name": "push_subscriptions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "endpoint": { + "name": "endpoint", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "p256dh": { + "name": "p256dh", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "auth": { + "name": "auth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "push_subscriptions_endpoint_unique": { + "name": "push_subscriptions_endpoint_unique", + "columns": [ + "endpoint" + ], + "isUnique": true + } + }, + "foreignKeys": { + "push_subscriptions_user_id_users_id_fk": { + "name": "push_subscriptions_user_id_users_id_fk", + "tableFrom": "push_subscriptions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "sessions": { + "name": "sessions", + "columns": { + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tags": { + "name": "tags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'accent'" + }, + "is_default": { + "name": "is_default", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "tags_name_unique": { + "name": "tags_name_unique", + "columns": [ + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "telegram_config": { + "name": "telegram_config", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "bot_token_enc": { + "name": "bot_token_enc", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "bot_username": { + "name": "bot_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "verified_at": { + "name": "verified_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "ticket_tags": { + "name": "ticket_tags", + "columns": { + "ticket_id": { + "name": "ticket_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tag_id": { + "name": "tag_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "ticket_tags_ticket_id_tickets_id_fk": { + "name": "ticket_tags_ticket_id_tickets_id_fk", + "tableFrom": "ticket_tags", + "tableTo": "tickets", + "columnsFrom": [ + "ticket_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "ticket_tags_tag_id_tags_id_fk": { + "name": "ticket_tags_tag_id_tags_id_fk", + "tableFrom": "ticket_tags", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "ticket_tags_ticket_id_tag_id_pk": { + "columns": [ + "ticket_id", + "tag_id" + ], + "name": "ticket_tags_ticket_id_tag_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tickets": { + "name": "tickets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'new'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'normal'" + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "customer_id": { + "name": "customer_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assignee_id": { + "name": "assignee_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_message_at": { + "name": "last_message_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": {}, + "foreignKeys": { + "tickets_customer_id_customers_id_fk": { + "name": "tickets_customer_id_customers_id_fk", + "tableFrom": "tickets", + "tableTo": "customers", + "columnsFrom": [ + "customer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tickets_assignee_id_users_id_fk": { + "name": "tickets_assignee_id_users_id_fk", + "tableFrom": "tickets", + "tableTo": "users", + "columnsFrom": [ + "assignee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'agent'" + }, + "auth_source": { + "name": "auth_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "widget_sites": { + "name": "widget_sites", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "site_key": { + "name": "site_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_origin": { + "name": "allowed_origin", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "enabled": { + "name": "enabled", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch('subsec') * 1000)" + } + }, + "indexes": { + "widget_sites_site_key_unique": { + "name": "widget_sites_site_key_unique", + "columns": [ + "site_key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/src/lib/db/migrations/meta/_journal.json b/src/lib/db/migrations/meta/_journal.json index b230d00..05ad199 100644 --- a/src/lib/db/migrations/meta/_journal.json +++ b/src/lib/db/migrations/meta/_journal.json @@ -57,6 +57,13 @@ "when": 1787124609684, "tag": "0007_neat_archangel", "breakpoints": true + }, + { + "idx": 8, + "version": "6", + "when": 1787126651618, + "tag": "0008_quick_ultragirl", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 2e8a77e..074215d 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -205,6 +205,10 @@ export const tags = sqliteTable("tags", { color: text("color", { enum: ["accent", "success", "warning", "info", "rose", "danger"] }) .notNull() .default("accent"), + // At most one tag has this set (enforced in service.ts's setDefaultTag, + // not at the schema level) — auto-attached to every ticket the moment + // it's created, across every channel (email/telegram/widget/manual). + isDefault: integer("is_default", { mode: "boolean" }).notNull().default(false), createdAt: timestamps.createdAt, }); diff --git a/src/lib/tickets/service.ts b/src/lib/tickets/service.ts index 1072688..9f4034d 100644 --- a/src/lib/tickets/service.ts +++ b/src/lib/tickets/service.ts @@ -89,7 +89,7 @@ async function getTagsForTickets(ticketIds: string[]): Promise { const rows = await db.query.tags.findMany({ orderBy: tagsTable.name }); - return rows.map((r) => ({ id: r.id, name: r.name, color: r.color })); + return rows.map((r) => ({ id: r.id, name: r.name, color: r.color, isDefault: r.isDefault })); } export async function createTag(name: string, color: TagDTO["color"]) { @@ -697,6 +704,20 @@ export async function deleteTag(id: string) { await db.delete(tagsTable).where(eq(tagsTable.id, id)); } +/** At most one default at a time — pass null to clear it entirely. */ +export async function setDefaultTag(id: string | null): Promise { + await db.update(tagsTable).set({ isDefault: false }).where(eq(tagsTable.isDefault, true)); + if (id) { + await db.update(tagsTable).set({ isDefault: true }).where(eq(tagsTable.id, id)); + } + return listTags(); +} + +async function getDefaultTagId(): Promise { + const row = await db.query.tags.findFirst({ where: eq(tagsTable.isDefault, true) }); + return row?.id ?? null; +} + /** Full replace — the ticket ends up tagged with exactly this set. */ export async function setTicketTags(ticketId: string, tagIds: string[]): Promise { await db.delete(ticketTags).where(eq(ticketTags.ticketId, ticketId)); diff --git a/src/lib/tickets/types.ts b/src/lib/tickets/types.ts index d3d1212..1c07131 100644 --- a/src/lib/tickets/types.ts +++ b/src/lib/tickets/types.ts @@ -10,6 +10,7 @@ export interface TagDTO { id: string; name: string; color: TagColor; + isDefault: boolean; } export interface TicketDTO {
Тегов пока нет.
+ Тег по умолчанию отмечается звёздочкой — он автоматически ставится на каждую новую заявку. +