Fix attachment previews and comment permissions
This commit is contained in:
parent
1cccb852a5
commit
e491becbc4
4 changed files with 62 additions and 11 deletions
|
|
@ -723,6 +723,10 @@ export const updateComment = mutation({
|
||||||
throw new ConvexError("Ticket não encontrado")
|
throw new ConvexError("Ticket não encontrado")
|
||||||
}
|
}
|
||||||
const ticketDoc = ticket as Doc<"tickets">
|
const ticketDoc = ticket as Doc<"tickets">
|
||||||
|
const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
|
||||||
|
if (!actor || actor.tenantId !== ticketDoc.tenantId) {
|
||||||
|
throw new ConvexError("Autor do comentário inválido")
|
||||||
|
}
|
||||||
const comment = await ctx.db.get(commentId);
|
const comment = await ctx.db.get(commentId);
|
||||||
if (!comment || comment.ticketId !== ticketId) {
|
if (!comment || comment.ticketId !== ticketId) {
|
||||||
throw new ConvexError("Comentário não encontrado");
|
throw new ConvexError("Comentário não encontrado");
|
||||||
|
|
@ -730,8 +734,15 @@ export const updateComment = mutation({
|
||||||
if (comment.authorId !== actorId) {
|
if (comment.authorId !== actorId) {
|
||||||
throw new ConvexError("Você não tem permissão para editar este comentário");
|
throw new ConvexError("Você não tem permissão para editar este comentário");
|
||||||
}
|
}
|
||||||
|
const normalizedRole = (actor.role ?? "AGENT").toUpperCase()
|
||||||
if (ticketDoc.requesterId === actorId) {
|
if (ticketDoc.requesterId === actorId) {
|
||||||
await requireCustomer(ctx, actorId, ticketDoc.tenantId)
|
if (normalizedRole === "CUSTOMER") {
|
||||||
|
await requireCustomer(ctx, actorId, ticketDoc.tenantId)
|
||||||
|
} else if (STAFF_ROLES.has(normalizedRole)) {
|
||||||
|
await requireTicketStaff(ctx, actorId, ticketDoc)
|
||||||
|
} else {
|
||||||
|
throw new ConvexError("Autor não possui permissão para editar")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await requireTicketStaff(ctx, actorId, ticketDoc)
|
await requireTicketStaff(ctx, actorId, ticketDoc)
|
||||||
}
|
}
|
||||||
|
|
@ -742,7 +753,6 @@ export const updateComment = mutation({
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null;
|
|
||||||
await ctx.db.insert("ticketEvents", {
|
await ctx.db.insert("ticketEvents", {
|
||||||
ticketId,
|
ticketId,
|
||||||
type: "COMMENT_EDITED",
|
type: "COMMENT_EDITED",
|
||||||
|
|
@ -772,6 +782,10 @@ export const removeCommentAttachment = mutation({
|
||||||
throw new ConvexError("Ticket não encontrado")
|
throw new ConvexError("Ticket não encontrado")
|
||||||
}
|
}
|
||||||
const ticketDoc = ticket as Doc<"tickets">
|
const ticketDoc = ticket as Doc<"tickets">
|
||||||
|
const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null
|
||||||
|
if (!actor || actor.tenantId !== ticketDoc.tenantId) {
|
||||||
|
throw new ConvexError("Autor do comentário inválido")
|
||||||
|
}
|
||||||
const comment = await ctx.db.get(commentId);
|
const comment = await ctx.db.get(commentId);
|
||||||
if (!comment || comment.ticketId !== ticketId) {
|
if (!comment || comment.ticketId !== ticketId) {
|
||||||
throw new ConvexError("Comentário não encontrado");
|
throw new ConvexError("Comentário não encontrado");
|
||||||
|
|
@ -780,8 +794,15 @@ export const removeCommentAttachment = mutation({
|
||||||
throw new ConvexError("Você não pode alterar anexos de outro usuário")
|
throw new ConvexError("Você não pode alterar anexos de outro usuário")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const normalizedRole = (actor.role ?? "AGENT").toUpperCase()
|
||||||
if (ticketDoc.requesterId === actorId) {
|
if (ticketDoc.requesterId === actorId) {
|
||||||
await requireCustomer(ctx, actorId, ticketDoc.tenantId)
|
if (normalizedRole === "CUSTOMER") {
|
||||||
|
await requireCustomer(ctx, actorId, ticketDoc.tenantId)
|
||||||
|
} else if (STAFF_ROLES.has(normalizedRole)) {
|
||||||
|
await requireTicketStaff(ctx, actorId, ticketDoc)
|
||||||
|
} else {
|
||||||
|
throw new ConvexError("Autor não possui permissão para alterar anexos")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await requireTicketStaff(ctx, actorId, ticketDoc)
|
await requireTicketStaff(ctx, actorId, ticketDoc)
|
||||||
}
|
}
|
||||||
|
|
@ -800,7 +821,6 @@ export const removeCommentAttachment = mutation({
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
const actor = (await ctx.db.get(actorId)) as Doc<"users"> | null;
|
|
||||||
await ctx.db.insert("ticketEvents", {
|
await ctx.db.insert("ticketEvents", {
|
||||||
ticketId,
|
ticketId,
|
||||||
type: "ATTACHMENT_REMOVED",
|
type: "ATTACHMENT_REMOVED",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { AppShell } from "@/components/app-shell"
|
||||||
import { SiteHeader } from "@/components/site-header"
|
import { SiteHeader } from "@/components/site-header"
|
||||||
import { TicketDetailView } from "@/components/tickets/ticket-detail-view"
|
import { TicketDetailView } from "@/components/tickets/ticket-detail-view"
|
||||||
import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static"
|
import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static"
|
||||||
import { NewTicketDialog } from "@/components/tickets/new-ticket-dialog"
|
import { NewTicketDialogDeferred } from "@/components/tickets/new-ticket-dialog.client"
|
||||||
import { getTicketById } from "@/lib/mocks/tickets"
|
import { getTicketById } from "@/lib/mocks/tickets"
|
||||||
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
import type { TicketWithDetails } from "@/lib/schemas/ticket"
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ export default async function TicketDetailPage({ params }: TicketDetailPageProps
|
||||||
title={`Ticket #${id}`}
|
title={`Ticket #${id}`}
|
||||||
lead={"Detalhes do ticket"}
|
lead={"Detalhes do ticket"}
|
||||||
secondaryAction={<SiteHeader.SecondaryButton>Compartilhar</SiteHeader.SecondaryButton>}
|
secondaryAction={<SiteHeader.SecondaryButton>Compartilhar</SiteHeader.SecondaryButton>}
|
||||||
primaryAction={<NewTicketDialog />}
|
primaryAction={<NewTicketDialogDeferred />}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
30
src/components/tickets/new-ticket-dialog.client.tsx
Normal file
30
src/components/tickets/new-ticket-dialog.client.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
|
||||||
|
import { NewTicketDialog } from "./new-ticket-dialog"
|
||||||
|
|
||||||
|
export function NewTicketDialogDeferred() {
|
||||||
|
const [mounted, setMounted] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="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-[#18181b]/30"
|
||||||
|
disabled
|
||||||
|
aria-disabled
|
||||||
|
>
|
||||||
|
Novo ticket
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return <NewTicketDialog />
|
||||||
|
}
|
||||||
|
|
@ -31,18 +31,19 @@ export function Dropzone({
|
||||||
|
|
||||||
const startUpload = useCallback(async (files: FileList | File[]) => {
|
const startUpload = useCallback(async (files: FileList | File[]) => {
|
||||||
const list = Array.from(files).slice(0, maxFiles);
|
const list = Array.from(files).slice(0, maxFiles);
|
||||||
const url = await generateUrl({});
|
|
||||||
const uploaded: Uploaded[] = [];
|
const uploaded: Uploaded[] = [];
|
||||||
for (const file of list) {
|
for (const file of list) {
|
||||||
if (file.size > maxSize) continue;
|
if (file.size > maxSize) continue;
|
||||||
|
const url = await generateUrl({});
|
||||||
const id = `${file.name}-${file.size}-${Date.now()}`;
|
const id = `${file.name}-${file.size}-${Date.now()}`;
|
||||||
const localPreview = file.type.startsWith("image/") ? URL.createObjectURL(file) : undefined;
|
const localPreview = file.type.startsWith("image/") ? URL.createObjectURL(file) : undefined;
|
||||||
setItems((prev) => [...prev, { id, name: file.name, progress: 0, status: "uploading" }]);
|
setItems((prev) => [...prev, { id, name: file.name, progress: 0, status: "uploading" }]);
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const form = new FormData();
|
|
||||||
form.append("file", file);
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open("POST", url);
|
xhr.open("POST", url);
|
||||||
|
if (file.type) {
|
||||||
|
xhr.setRequestHeader("Content-Type", file.type);
|
||||||
|
}
|
||||||
xhr.upload.onprogress = (e) => {
|
xhr.upload.onprogress = (e) => {
|
||||||
if (!e.lengthComputable) return;
|
if (!e.lengthComputable) return;
|
||||||
const progress = Math.round((e.loaded / e.total) * 100);
|
const progress = Math.round((e.loaded / e.total) * 100);
|
||||||
|
|
@ -66,7 +67,7 @@ export function Dropzone({
|
||||||
setItems((prev) => prev.map((it) => (it.id === id ? { ...it, status: "error" } : it)));
|
setItems((prev) => prev.map((it) => (it.id === id ? { ...it, status: "error" } : it)));
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
xhr.send(form);
|
xhr.send(file);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (uploaded.length) onUploaded?.(uploaded);
|
if (uploaded.length) onUploaded?.(uploaded);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue