1
0
Fork 0
polaris-task-force/src/components/frontend/helpdesk/CreateTicketDialog.tsx
Z8MB1E c80ecf9141 feat(helpdesk): add helpdesk UI components
Add TicketsView, TicketCard, TicketDetail, TicketTimeline, CreateTicketDialog, and TicketBadges components.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-08-16 18:12:44 -04:00

236 lines
7.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { PlusIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { createTicket } from "@/app/(frontend)/helpdesk/actions";
import {
TICKET_CATEGORIES,
TICKET_PRIORITIES,
type TicketCategory,
type TicketPriority,
} from "@/lib/tickets/ticketMeta";
import { cn } from "@/lib/utils";
function isTicketCategory(value: string): value is TicketCategory {
return TICKET_CATEGORIES.some((option) => option.value === value);
}
function isTicketPriority(value: string): value is TicketPriority {
return TICKET_PRIORITIES.some((option) => option.value === value);
}
export function CreateTicketDialog() {
const router = useRouter();
const [open, setOpen] = useState(false);
const [title, setTitle] = useState("");
const [category, setCategory] = useState<TicketCategory>("support_request");
const [priority, setPriority] = useState<TicketPriority>("medium");
const [description, setDescription] = useState("");
const [pending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
function reset() {
setTitle("");
setCategory("support_request");
setPriority("medium");
setDescription("");
setPending(false);
setError(null);
}
async function handleSubmit() {
const trimmedTitle = title.trim();
const trimmedDescription = description.trim();
if (!trimmedTitle) {
setError("Add a short title for your request.");
return;
}
if (!trimmedDescription) {
setError("Describe the issue or request so the team can help.");
return;
}
setPending(true);
setError(null);
const result = await createTicket({
title: trimmedTitle,
description: trimmedDescription,
category,
priority,
});
if (result.success) {
setOpen(false);
reset();
toast.success("Ticket created");
router.refresh();
} else {
setError(result.error ?? "Could not create the ticket.");
setPending(false);
}
}
const getColorForCategoryLabel = (label: string) => {
switch (label.toLowerCase()) {
case "support_request":
return "text-blue-300";
case "feature_request":
return "text-green-300";
case "bug_report":
return "text-red-300";
default:
return "text-muted-foreground";
}
};
const getColorForPriorityLabel = (label: string) => {
switch (label.toLowerCase()) {
case "low":
return "text-zinc-300";
case "medium":
return "text-blue-300";
case "high":
return "text-amber-300";
case "critical":
return "text-red-300";
default:
return "text-muted-foreground";
}
}
return (
<Dialog
open={open}
onOpenChange={(nextOpen) => {
setOpen(nextOpen);
if (!nextOpen) reset();
}}
>
<DialogTrigger asChild>
<Button size="sm">
<PlusIcon className="size-4" />
New ticket
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>New ticket</DialogTitle>
<DialogDescription>
Share the details of what you need help with. The helpdesk will follow up here.
</DialogDescription>
</DialogHeader>
<form
className="flex flex-col gap-4"
onSubmit={(event) => {
event.preventDefault();
void handleSubmit();
}}
>
<div className="flex flex-col gap-1.5">
<Label htmlFor="ticket-title">Title</Label>
<Input
id="ticket-title"
placeholder="What do you need help with?"
value={title}
onChange={(event) => setTitle(event.target.value)}
disabled={pending}
/>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<div className="flex flex-col gap-1.5">
<Label htmlFor="ticket-category">Category</Label>
<Select
value={category}
onValueChange={(value) => {
if (isTicketCategory(value)) setCategory(value);
}}
disabled={pending}
>
<SelectTrigger id="ticket-category" className={cn("w-full", getColorForCategoryLabel(category))}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{TICKET_CATEGORIES.map((option) => (
<SelectItem key={option.value} value={option.value} className={getColorForCategoryLabel(option.value)}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="ticket-priority">Priority</Label>
<Select
value={priority}
onValueChange={(value) => {
if (isTicketPriority(value)) setPriority(value);
}}
disabled={pending}
>
<SelectTrigger id="ticket-priority" className={cn("w-full", getColorForPriorityLabel(priority))}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{TICKET_PRIORITIES.map((option) => (
<SelectItem key={option.value} value={option.value} className={getColorForPriorityLabel(option.value)}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="flex flex-col gap-1.5">
<Message title="Please provide a description" message="Include what happened, what you expected, and any useful context." />
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="ticket-description">Description</Label>
<Textarea
id="ticket-description"
rows={5}
placeholder="Include what happened, what you expected, and any useful context."
value={description}
onChange={(event) => setDescription(event.target.value)}
disabled={pending}
/>
</div>
{error && (
<p role="alert" className="text-sm text-red-400">
{error}
</p>
)}
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={pending}>
Cancel
</Button>
<Button type="submit" disabled={pending}>
{pending ? "Creating..." : "Create ticket"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}