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:
parent
98b23af4b2
commit
0f3ba07a5e
10 changed files with 446 additions and 76 deletions
|
|
@ -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()),
|
||||
}),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
})
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue