feat(helpdesk): add ticket domain libs
Add shared ticket metadata (labels, styles), staff resolution, and plaintext-to-lexical conversion helpers. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
e7eff028b7
commit
ff986bf4ec
3 changed files with 227 additions and 0 deletions
89
src/lib/tickets/lexical.ts
Normal file
89
src/lib/tickets/lexical.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* Converts plain text (from client textareas) into Payload-compatible lexical
|
||||
* editor state so richText fields can be populated from simple forms.
|
||||
*
|
||||
* Paragraph breaks: double newlines split paragraphs; single newlines become
|
||||
* line breaks inside a paragraph.
|
||||
*/
|
||||
|
||||
interface LexicalTextNode {
|
||||
type: "text";
|
||||
version: 1;
|
||||
detail: 0;
|
||||
format: 0;
|
||||
mode: "normal";
|
||||
style: "";
|
||||
text: string;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
interface LexicalLinebreakNode {
|
||||
type: "linebreak";
|
||||
version: 1;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
type LexicalInlineNode = LexicalTextNode | LexicalLinebreakNode;
|
||||
|
||||
interface LexicalParagraphNode {
|
||||
type: "paragraph";
|
||||
version: 1;
|
||||
direction: "ltr";
|
||||
format: "";
|
||||
indent: 0;
|
||||
textFormat: 0;
|
||||
textStyle: "";
|
||||
children: LexicalInlineNode[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PlaintextLexicalState {
|
||||
root: {
|
||||
type: "root";
|
||||
version: 1;
|
||||
direction: "ltr";
|
||||
format: "";
|
||||
indent: 0;
|
||||
children: LexicalParagraphNode[];
|
||||
};
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
function inlineNodes(text: string): LexicalInlineNode[] {
|
||||
const nodes: LexicalInlineNode[] = [];
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].length > 0) {
|
||||
nodes.push({ type: "text", version: 1, detail: 0, format: 0, mode: "normal", style: "", text: lines[i] });
|
||||
}
|
||||
if (i < lines.length - 1) {
|
||||
nodes.push({ type: "linebreak", version: 1 });
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export function plaintextToLexical(text: string): PlaintextLexicalState {
|
||||
const trimmed = text.trim();
|
||||
const paragraphs = trimmed.length === 0 ? [""] : trimmed.split(/\n{2,}/);
|
||||
|
||||
return {
|
||||
root: {
|
||||
type: "root",
|
||||
version: 1,
|
||||
direction: "ltr",
|
||||
format: "",
|
||||
indent: 0,
|
||||
children: paragraphs.map((para) => ({
|
||||
type: "paragraph",
|
||||
version: 1,
|
||||
direction: "ltr",
|
||||
format: "",
|
||||
indent: 0,
|
||||
textFormat: 0,
|
||||
textStyle: "",
|
||||
children: inlineNodes(para),
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
26
src/lib/tickets/staff.ts
Normal file
26
src/lib/tickets/staff.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* Helpdesk staff resolution. "Staff" = users with the `admin` or `developer`
|
||||
* role — the people who can manage tickets. Used to fan out notifications when
|
||||
* a new ticket is created.
|
||||
*/
|
||||
|
||||
type PayloadType = Awaited<ReturnType<typeof import("payload").getPayload>>;
|
||||
|
||||
export async function getStaffUserIds(payload: PayloadType): Promise<number[]> {
|
||||
try {
|
||||
const res = await payload.find({
|
||||
collection: "users",
|
||||
where: {
|
||||
or: [{ roles: { contains: "admin" } }, { roles: { contains: "developer" } }],
|
||||
},
|
||||
limit: 50,
|
||||
depth: 0,
|
||||
select: { username: true },
|
||||
overrideAccess: true,
|
||||
});
|
||||
return res.docs.map((d) => d.id);
|
||||
} catch (err) {
|
||||
console.error("[Tickets] Failed to resolve staff recipients:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
112
src/lib/tickets/ticketMeta.ts
Normal file
112
src/lib/tickets/ticketMeta.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Shared ticket vocabulary: status/priority/category options, human labels, and
|
||||
* UI style maps. Single source of truth used by the collection, server actions,
|
||||
* and the frontend.
|
||||
*/
|
||||
|
||||
export const TICKET_STATUSES = [
|
||||
{ label: "Open", value: "open" },
|
||||
{ label: "In Review", value: "in_review" },
|
||||
{ label: "Planning", value: "planning" },
|
||||
{ label: "In Progress", value: "in_progress" },
|
||||
{ label: "Waiting on Reporter", value: "waiting_on_reporter" },
|
||||
{ label: "In Testing", value: "in_testing" },
|
||||
{ label: "Resolved", value: "resolved" },
|
||||
{ label: "Canceled", value: "canceled" },
|
||||
{ label: "Closed", value: "closed" },
|
||||
] as const;
|
||||
|
||||
export type TicketStatus = (typeof TICKET_STATUSES)[number]["value"];
|
||||
|
||||
export const TICKET_PRIORITIES = [
|
||||
{ label: "Low", value: "low" },
|
||||
{ label: "Medium", value: "medium" },
|
||||
{ label: "High", value: "high" },
|
||||
{ label: "Critical", value: "critical" },
|
||||
] as const;
|
||||
|
||||
export type TicketPriority = (typeof TICKET_PRIORITIES)[number]["value"];
|
||||
|
||||
export const TICKET_CATEGORIES = [
|
||||
{ label: "Bug Report", value: "bug_report" },
|
||||
{ label: "Feature Request", value: "feature_request" },
|
||||
{ label: "Support Request", value: "support_request" },
|
||||
{ label: "Other", value: "other" },
|
||||
] as const;
|
||||
|
||||
export type TicketCategory = (typeof TICKET_CATEGORIES)[number]["value"];
|
||||
|
||||
export const STATUS_LABEL: Record<TicketStatus, string> = {
|
||||
open: "Open",
|
||||
in_review: "In Review",
|
||||
planning: "Planning",
|
||||
in_progress: "In Progress",
|
||||
waiting_on_reporter: "Waiting on Reporter",
|
||||
in_testing: "In Testing",
|
||||
resolved: "Resolved",
|
||||
canceled: "Canceled",
|
||||
closed: "Closed",
|
||||
};
|
||||
|
||||
export const PRIORITY_LABEL: Record<TicketPriority, string> = {
|
||||
low: "Low",
|
||||
medium: "Medium",
|
||||
high: "High",
|
||||
critical: "Critical",
|
||||
};
|
||||
|
||||
export const CATEGORY_LABEL: Record<TicketCategory, string> = {
|
||||
bug_report: "Bug Report",
|
||||
feature_request: "Feature Request",
|
||||
support_request: "Support Request",
|
||||
other: "Other",
|
||||
};
|
||||
|
||||
export const statusLabel = (value: string | null | undefined): string =>
|
||||
value && value in STATUS_LABEL
|
||||
? STATUS_LABEL[value as TicketStatus]
|
||||
: value ?? "Unknown";
|
||||
|
||||
export const priorityLabel = (value: string | null | undefined): string =>
|
||||
value && value in PRIORITY_LABEL
|
||||
? PRIORITY_LABEL[value as TicketPriority]
|
||||
: value ?? "Unknown";
|
||||
|
||||
export const categoryLabel = (value: string | null | undefined): string =>
|
||||
value && value in CATEGORY_LABEL
|
||||
? CATEGORY_LABEL[value as TicketCategory]
|
||||
: value ?? "Unknown";
|
||||
|
||||
/** Statuses that count as the ticket being done — sets `closedAt`. */
|
||||
export const CLOSING_STATUSES: readonly TicketStatus[] = [
|
||||
"resolved",
|
||||
"canceled",
|
||||
"closed",
|
||||
];
|
||||
|
||||
/** Badge color classes per status, keyed for use with the shadcn `Badge` component. */
|
||||
export const STATUS_STYLES: Record<TicketStatus, string> = {
|
||||
open: "bg-sky-500/15 text-sky-400 border-sky-500/30",
|
||||
in_review: "bg-violet-500/15 text-violet-400 border-violet-500/30",
|
||||
planning: "bg-amber-500/15 text-amber-400 border-amber-500/30",
|
||||
in_progress: "bg-blue-500/15 text-blue-400 border-blue-500/30",
|
||||
waiting_on_reporter: "bg-orange-500/15 text-orange-400 border-orange-500/30",
|
||||
in_testing: "bg-cyan-500/15 text-cyan-400 border-cyan-500/30",
|
||||
resolved: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30",
|
||||
canceled: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30",
|
||||
closed: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30",
|
||||
};
|
||||
|
||||
export const PRIORITY_STYLES: Record<TicketPriority, string> = {
|
||||
low: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30",
|
||||
medium: "bg-sky-500/15 text-sky-400 border-sky-500/30",
|
||||
high: "bg-amber-500/15 text-amber-400 border-amber-500/30",
|
||||
critical: "bg-red-500/15 text-red-400 border-red-500/30",
|
||||
};
|
||||
|
||||
export const CATEGORY_STYLES: Record<TicketCategory, string> = {
|
||||
bug_report: "bg-red-500/15 text-red-400 border-red-500/30",
|
||||
feature_request: "bg-violet-500/15 text-violet-400 border-violet-500/30",
|
||||
support_request: "bg-sky-500/15 text-sky-400 border-sky-500/30",
|
||||
other: "bg-zinc-500/15 text-zinc-400 border-zinc-500/30",
|
||||
};
|
||||
Loading…
Reference in a new issue