feat(checklist): adiciona tipo pergunta e descricao nos itens

- Adiciona campo `type` (checkbox/question) nos itens do checklist
- Adiciona campo `description` para descricao do item
- Adiciona campo `options` para opcoes de resposta em perguntas
- Adiciona campo `answer` para resposta selecionada
- Atualiza UI para mostrar descricao e opcoes de pergunta
- Cria componente radio-group para selecao de respostas
- Adiciona mutation setChecklistItemAnswer para salvar respostas

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rever-tecnologia 2025-12-15 16:27:23 -03:00
parent 98b23af4b2
commit 0f3ba07a5e
10 changed files with 446 additions and 76 deletions

View file

@ -14,17 +14,37 @@ function normalizeTemplateDescription(input: string | null | undefined) {
return text.length > 0 ? text : null
}
type ChecklistItemType = "checkbox" | "question"
type RawTemplateItem = {
id?: string
text: string
description?: string
type?: string
options?: string[]
required?: boolean
}
type NormalizedTemplateItem = {
id: string
text: string
description?: string
type?: ChecklistItemType
options?: string[]
required?: boolean
}
function normalizeTemplateItems(
raw: Array<{ id?: string; text: string; required?: boolean }>,
raw: RawTemplateItem[],
options: { generateId?: () => string }
) {
): NormalizedTemplateItem[] {
if (!Array.isArray(raw) || raw.length === 0) {
throw new ConvexError("Adicione pelo menos um item no checklist.")
}
const generateId = options.generateId ?? (() => crypto.randomUUID())
const seen = new Set<string>()
const items: Array<{ id: string; text: string; required?: boolean }> = []
const items: NormalizedTemplateItem[] = []
for (const entry of raw) {
const id = String(entry.id ?? "").trim() || generateId()
@ -38,11 +58,28 @@ function normalizeTemplateItems(
throw new ConvexError("Todos os itens do checklist precisam ter um texto.")
}
if (text.length > 240) {
throw new ConvexError("Item do checklist muito longo (máx. 240 caracteres).")
throw new ConvexError("Item do checklist muito longo (max. 240 caracteres).")
}
const description = entry.description?.trim() || undefined
const itemType: ChecklistItemType = entry.type === "question" ? "question" : "checkbox"
const itemOptions = itemType === "question" && Array.isArray(entry.options)
? entry.options.map((o) => String(o).trim()).filter((o) => o.length > 0)
: undefined
if (itemType === "question" && (!itemOptions || itemOptions.length < 2)) {
throw new ConvexError(`A pergunta "${text}" precisa ter pelo menos 2 opcoes.`)
}
const required = typeof entry.required === "boolean" ? entry.required : true
items.push({ id, text, required })
items.push({
id,
text,
description,
type: itemType,
options: itemOptions,
required,
})
}
return items
@ -57,6 +94,9 @@ function mapTemplate(template: Doc<"ticketChecklistTemplates">, company: Doc<"co
items: (template.items ?? []).map((item) => ({
id: item.id,
text: item.text,
description: item.description,
type: item.type ?? "checkbox",
options: item.options,
required: typeof item.required === "boolean" ? item.required : true,
})),
isArchived: Boolean(template.isArchived),
@ -164,6 +204,9 @@ export const create = mutation({
v.object({
id: v.optional(v.string()),
text: v.string(),
description: v.optional(v.string()),
type: v.optional(v.string()),
options: v.optional(v.array(v.string())),
required: v.optional(v.boolean()),
}),
),
@ -216,6 +259,9 @@ export const update = mutation({
v.object({
id: v.optional(v.string()),
text: v.string(),
description: v.optional(v.string()),
type: v.optional(v.string()),
options: v.optional(v.array(v.string())),
required: v.optional(v.boolean()),
}),
),

View file

@ -314,6 +314,10 @@ export default defineSchema({
v.object({
id: v.string(),
text: v.string(),
description: v.optional(v.string()),
type: v.optional(v.string()), // "checkbox" | "question"
options: v.optional(v.array(v.string())), // Para tipo "question": ["Sim", "Nao", ...]
answer: v.optional(v.string()), // Resposta selecionada para tipo "question"
done: v.boolean(),
required: v.optional(v.boolean()),
templateId: v.optional(v.id("ticketChecklistTemplates")),
@ -659,6 +663,9 @@ export default defineSchema({
v.object({
id: v.string(),
text: v.string(),
description: v.optional(v.string()),
type: v.optional(v.string()), // "checkbox" | "question"
options: v.optional(v.array(v.string())), // Para tipo "question": ["Sim", "Nao", ...]
required: v.optional(v.boolean()),
})
),

View file

@ -1,8 +1,14 @@
import type { Id } from "./_generated/dataModel"
export type ChecklistItemType = "checkbox" | "question"
export type TicketChecklistItem = {
id: string
text: string
description?: string
type?: ChecklistItemType
options?: string[] // Para tipo "question": ["Sim", "Nao", ...]
answer?: string // Resposta selecionada para tipo "question"
done: boolean
required?: boolean
templateId?: Id<"ticketChecklistTemplates">
@ -13,9 +19,18 @@ export type TicketChecklistItem = {
doneBy?: Id<"users">
}
export type TicketChecklistTemplateItem = {
id: string
text: string
description?: string
type?: string // "checkbox" | "question" - string para compatibilidade com schema
options?: string[]
required?: boolean
}
export type TicketChecklistTemplateLike = {
_id: Id<"ticketChecklistTemplates">
items: Array<{ id: string; text: string; required?: boolean }>
items: TicketChecklistTemplateItem[]
}
export function normalizeChecklistText(input: string) {
@ -53,9 +68,13 @@ export function applyChecklistTemplateToItems(
const key = `${String(template._id)}:${templateItemId}`
if (existingKeys.has(key)) continue
existingKeys.add(key)
const itemType = tplItem.type ?? "checkbox"
next.push({
id: generateId(),
text,
description: tplItem.description,
type: itemType as ChecklistItemType,
options: itemType === "question" ? tplItem.options : undefined,
done: false,
required: typeof tplItem.required === "boolean" ? tplItem.required : true,
templateId: template._id,

View file

@ -2669,6 +2669,49 @@ export const setChecklistItemRequired = mutation({
},
});
export const setChecklistItemAnswer = mutation({
args: {
ticketId: v.id("tickets"),
actorId: v.id("users"),
itemId: v.string(),
answer: v.string(),
},
handler: async (ctx, { ticketId, actorId, itemId, answer }) => {
const ticket = await ctx.db.get(ticketId);
if (!ticket) {
throw new ConvexError("Ticket não encontrado");
}
const ticketDoc = ticket as Doc<"tickets">;
await requireTicketStaff(ctx, actorId, ticketDoc);
const checklist = normalizeTicketChecklist(ticketDoc.checklist);
const index = checklist.findIndex((item) => item.id === itemId);
if (index < 0) {
throw new ConvexError("Item do checklist não encontrado.");
}
const item = checklist[index]!;
if (item.type !== "question") {
throw new ConvexError("Este item não é uma pergunta.");
}
const now = Date.now();
const normalizedAnswer = answer.trim();
const isDone = normalizedAnswer.length > 0;
const nextChecklist = checklist.map((it) => {
if (it.id !== itemId) return it;
if (isDone) {
return { ...it, answer: normalizedAnswer, done: true, doneAt: now, doneBy: actorId };
}
return { ...it, answer: undefined, done: false, doneAt: undefined, doneBy: undefined };
});
await ctx.db.patch(ticketId, { checklist: nextChecklist, updatedAt: now });
return { ok: true };
},
});
export const removeChecklistItem = mutation({
args: {
ticketId: v.id("tickets"),