feat: add ticket category model and align ticket ui\n\nCo-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
55511f3a0e
commit
fab1cbe476
17 changed files with 1121 additions and 42 deletions
|
|
@ -21,13 +21,14 @@ import { StatusSelect } from "@/components/tickets/status-select"
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { CategorySelectFields } from "@/components/tickets/category-select"
|
||||
|
||||
interface TicketHeaderProps {
|
||||
ticket: TicketWithDetails
|
||||
}
|
||||
|
||||
const cardClass = "relative space-y-4 rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"
|
||||
const referenceBadgeClass = "inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold text-neutral-700"
|
||||
const referenceBadgeClass = "inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700"
|
||||
const startButtonClass =
|
||||
"inline-flex items-center gap-1 rounded-lg border border-black bg-black px-3 py-1.5 text-sm font-semibold text-white transition hover:bg-[#18181b]/85 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-black/30"
|
||||
const pauseButtonClass =
|
||||
|
|
@ -64,6 +65,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const updateSummary = useMutation(api.tickets.updateSummary)
|
||||
const startWork = useMutation(api.tickets.startWork)
|
||||
const pauseWork = useMutation(api.tickets.pauseWork)
|
||||
const updateCategories = useMutation(api.tickets.updateCategories)
|
||||
const agents = (useQuery(api.users.listAgents, { tenantId: ticket.tenantId }) as Doc<"users">[] | undefined) ?? []
|
||||
const queues = (useQuery(api.queues.summary, { tenantId: ticket.tenantId }) as TicketQueueSummary[] | undefined) ?? []
|
||||
const [status] = useState<TicketStatus>(ticket.status)
|
||||
|
|
@ -79,6 +81,11 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
const [editing, setEditing] = useState(false)
|
||||
const [subject, setSubject] = useState(ticket.subject)
|
||||
const [summary, setSummary] = useState(ticket.summary ?? "")
|
||||
const [categorySelection, setCategorySelection] = useState({
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
subcategoryId: ticket.subcategory?.id ?? "",
|
||||
})
|
||||
const [savingCategory, setSavingCategory] = useState(false)
|
||||
const dirty = useMemo(
|
||||
() => subject !== ticket.subject || (summary ?? "") !== (ticket.summary ?? ""),
|
||||
[subject, summary, ticket.subject, ticket.summary]
|
||||
|
|
@ -107,6 +114,51 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
setEditing(false)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setCategorySelection({
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
subcategoryId: ticket.subcategory?.id ?? "",
|
||||
})
|
||||
}, [ticket.category?.id, ticket.subcategory?.id])
|
||||
|
||||
const categoryDirty = useMemo(() => {
|
||||
const currentCategory = ticket.category?.id ?? ""
|
||||
const currentSubcategory = ticket.subcategory?.id ?? ""
|
||||
return (
|
||||
categorySelection.categoryId !== currentCategory || categorySelection.subcategoryId !== currentSubcategory
|
||||
)
|
||||
}, [categorySelection.categoryId, categorySelection.subcategoryId, ticket.category?.id, ticket.subcategory?.id])
|
||||
|
||||
const handleResetCategory = () => {
|
||||
setCategorySelection({
|
||||
categoryId: ticket.category?.id ?? "",
|
||||
subcategoryId: ticket.subcategory?.id ?? "",
|
||||
})
|
||||
}
|
||||
|
||||
async function handleSaveCategory() {
|
||||
if (!userId) return
|
||||
if (!categorySelection.categoryId || !categorySelection.subcategoryId) {
|
||||
toast.error("Selecione uma categoria válida.")
|
||||
return
|
||||
}
|
||||
setSavingCategory(true)
|
||||
toast.loading("Atualizando categoria...", { id: "ticket-category" })
|
||||
try {
|
||||
await updateCategories({
|
||||
ticketId: ticket.id as Id<"tickets">,
|
||||
categoryId: categorySelection.categoryId as Id<"ticketCategories">,
|
||||
subcategoryId: categorySelection.subcategoryId as Id<"ticketSubcategories">,
|
||||
actorId: userId as Id<"users">,
|
||||
})
|
||||
toast.success("Categoria atualizada!", { id: "ticket-category" })
|
||||
} catch {
|
||||
toast.error("Não foi possível atualizar a categoria.", { id: "ticket-category" })
|
||||
} finally {
|
||||
setSavingCategory(false)
|
||||
}
|
||||
}
|
||||
|
||||
const workSummary = useMemo(() => {
|
||||
if (workSummaryRemote !== undefined) return workSummaryRemote ?? null
|
||||
if (!ticket.workSummary) return null
|
||||
|
|
@ -146,7 +198,12 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
|
||||
return (
|
||||
<div className={cardClass}>
|
||||
<div className="absolute right-6 top-6 flex items-center gap-2">
|
||||
<div className="absolute right-6 top-6 flex items-center gap-3">
|
||||
{workSummary ? (
|
||||
<Badge className="inline-flex h-9 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 text-sm font-semibold text-neutral-700">
|
||||
Tempo total: {formattedTotalWorked}
|
||||
</Badge>
|
||||
) : null}
|
||||
{!editing ? (
|
||||
<Button size="sm" className={editButtonClass} onClick={() => setEditing(true)}>
|
||||
Editar
|
||||
|
|
@ -201,9 +258,6 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
</div>
|
||||
{workSummary ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Badge className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold text-neutral-700">
|
||||
Tempo total: {formattedTotalWorked}
|
||||
</Badge>
|
||||
{isPlaying ? (
|
||||
<Badge className="inline-flex items-center gap-1 rounded-full border border-black bg-black px-3 py-1 text-xs font-semibold text-white">
|
||||
Sessão atual: {formattedCurrentSession}
|
||||
|
|
@ -246,6 +300,40 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
|
|||
</div>
|
||||
<Separator className="bg-slate-200" />
|
||||
<div className="grid gap-4 text-sm text-neutral-600 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="flex flex-col gap-2 sm:col-span-2 lg:col-span-3">
|
||||
<span className={sectionLabelClass}>Categorias</span>
|
||||
<CategorySelectFields
|
||||
tenantId={ticket.tenantId}
|
||||
autoSelectFirst={!ticket.category}
|
||||
categoryId={categorySelection.categoryId || null}
|
||||
subcategoryId={categorySelection.subcategoryId || null}
|
||||
onCategoryChange={(value) => {
|
||||
setCategorySelection((prev) => ({ ...prev, categoryId: value }))
|
||||
}}
|
||||
onSubcategoryChange={(value) => {
|
||||
setCategorySelection((prev) => ({ ...prev, subcategoryId: value }))
|
||||
}}
|
||||
/>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
className={startButtonClass}
|
||||
onClick={handleSaveCategory}
|
||||
disabled={!categoryDirty || savingCategory}
|
||||
>
|
||||
{savingCategory ? "Salvando..." : "Salvar"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-sm font-semibold text-neutral-700"
|
||||
onClick={handleResetCategory}
|
||||
disabled={savingCategory || !categoryDirty}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className={sectionLabelClass}>Solicitante</span>
|
||||
<span className={sectionValueClass}>{ticket.requester.name}</span>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue