From dded6d19278154df77fa6cb58c157f34a6f05aec Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Sat, 18 Oct 2025 01:15:15 -0300 Subject: [PATCH] =?UTF-8?q?Reorganiza=20gest=C3=A3o=20de=20usu=C3=A1rios?= =?UTF-8?q?=20e=20remove=20dados=20mock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex/seed.ts | 250 +--- src/app/dashboard/data.json | 614 -------- src/app/dashboard/page.tsx | 7 +- src/app/play/page.tsx | 34 +- src/app/tickets/[id]/page.tsx | 7 +- src/app/tickets/tickets-page-client.tsx | 2 +- src/components/admin/admin-users-manager.tsx | 416 +++++- .../admin/clients/admin-clients-manager.tsx | 8 +- .../companies/admin-companies-manager.tsx | 58 +- .../machines/admin-machines-overview.tsx | 44 +- .../portal/portal-ticket-detail.tsx | 17 +- src/components/section-cards.tsx | 10 +- .../tickets/play-next-ticket-card.tsx | 4 +- .../tickets/ticket-comments.rich.tsx | 17 +- .../tickets/ticket-detail-static.tsx | 21 - src/components/tickets/ticket-detail-view.tsx | 88 +- src/components/tickets/tickets-table.tsx | 14 +- src/components/ui/phone-input.tsx | 82 +- src/lib/country-codes.ts | 1234 +++++++++++++++++ src/lib/mocks/tickets.ts | 304 ---- 20 files changed, 1863 insertions(+), 1368 deletions(-) delete mode 100644 src/app/dashboard/data.json delete mode 100644 src/components/tickets/ticket-detail-static.tsx create mode 100644 src/lib/country-codes.ts delete mode 100644 src/lib/mocks/tickets.ts diff --git a/convex/seed.ts b/convex/seed.ts index 2e280e6..a26f12b 100644 --- a/convex/seed.ts +++ b/convex/seed.ts @@ -1,4 +1,3 @@ -import { randomBytes } from "@noble/hashes/utils" import type { Id } from "./_generated/dataModel" import { mutation } from "./_generated/server" @@ -59,17 +58,6 @@ export const seedDemo = mutation({ } // Ensure users - function slugify(value: string) { - return value - .normalize("NFD") - .replace(/[\u0300-\u036f]/g, "") - .replace(/[^\w\s-]/g, "") - .trim() - .replace(/\s+/g, "-") - .replace(/-+/g, "-") - .toLowerCase(); - } - function defaultAvatar(name: string, email: string, role: string) { const normalizedRole = role.toUpperCase(); if (normalizedRole === "MANAGER") { @@ -78,54 +66,6 @@ export const seedDemo = mutation({ const first = name.split(" ")[0] ?? email; return `https://avatar.vercel.sh/${encodeURIComponent(first)}`; } - - async function ensureCompany(def: { - name: string; - slug?: string; - cnpj?: string; - domain?: string; - phone?: string; - description?: string; - address?: string; - provisioningCode?: string; - }): Promise> { - const slug = def.slug ?? slugify(def.name); - const existing = await ctx.db - .query("companies") - .withIndex("by_tenant_slug", (q) => q.eq("tenantId", tenantId).eq("slug", slug)) - .first(); - const now = Date.now(); - const payload = { - tenantId, - name: def.name, - slug, - provisioningCode: def.provisioningCode ?? existing?.provisioningCode ?? generateCode(), - cnpj: def.cnpj ?? undefined, - domain: def.domain ?? undefined, - phone: def.phone ?? undefined, - description: def.description ?? undefined, - address: def.address ?? undefined, - createdAt: now, - updatedAt: now, - }; - if (existing) { - const updates: Record = {}; - if (existing.name !== payload.name) updates.name = payload.name; - if (existing.cnpj !== payload.cnpj) updates.cnpj = payload.cnpj; - if (existing.domain !== payload.domain) updates.domain = payload.domain; - if (existing.phone !== payload.phone) updates.phone = payload.phone; - if (existing.description !== payload.description) updates.description = payload.description; - if (existing.address !== payload.address) updates.address = payload.address; - if (existing.provisioningCode !== payload.provisioningCode) updates.provisioningCode = payload.provisioningCode; - if (Object.keys(updates).length > 0) { - updates.updatedAt = now; - await ctx.db.patch(existing._id, updates); - } - return existing._id; - } - return await ctx.db.insert("companies", payload); - } - async function ensureUser(params: { name: string; email: string; @@ -161,42 +101,6 @@ export const seedDemo = mutation({ }); } - const companiesSeed: Array<{ - name: string; - slug: string; - cnpj?: string; - domain?: string; - phone?: string; - description?: string; - address?: string; - provisioningCode?: string; - }> = [ - { - name: "Atlas Engenharia Digital", - slug: "atlas-engenharia", - cnpj: "12.345.678/0001-90", - domain: "atlasengenharia.com.br", - phone: "+55 11 4002-8922", - description: "Transformação digital para empresas de engenharia e construção.", - address: "Av. Paulista, 1234 - Bela Vista, São Paulo/SP", - }, - { - name: "Omni Saúde Integrada", - slug: "omni-saude", - cnpj: "45.678.912/0001-05", - domain: "omnisaude.com.br", - phone: "+55 31 3555-7788", - description: "Rede de clínicas com serviços de telemedicina e prontuário eletrônico.", - address: "Rua da Bahia, 845 - Centro, Belo Horizonte/MG", - }, - ]; - - const companyIds = new Map>(); - for (const company of companiesSeed) { - const id = await ensureCompany(company); - companyIds.set(company.slug, id); - } - const adminId = await ensureUser({ name: "Administrador", email: "admin@sistema.dev", role: "ADMIN" }); const staffRoster = [ { name: "Gabriel Oliveira", email: "gabriel.oliveira@rever.com.br" }, @@ -209,62 +113,9 @@ export const seedDemo = mutation({ { name: "Weslei Magalhães", email: "weslei@rever.com.br" }, ]; - const staffIds = await Promise.all( + await Promise.all( staffRoster.map((staff) => ensureUser({ name: staff.name, email: staff.email, role: "AGENT" })), ); - const defaultAssigneeId = staffIds[0] ?? adminId; - - const atlasCompanyId = companyIds.get("atlas-engenharia"); - const omniCompanyId = companyIds.get("omni-saude"); - if (!atlasCompanyId || !omniCompanyId) { - throw new Error("Empresas padrão não foram inicializadas"); - } - - const atlasManagerId = await ensureUser({ - name: "Mariana Andrade", - email: "mariana.andrade@atlasengenharia.com.br", - role: "MANAGER", - companyId: atlasCompanyId, - }); - - const joaoAtlasId = await ensureUser({ - name: "João Pedro Ramos", - email: "joao.ramos@atlasengenharia.com.br", - role: "MANAGER", - companyId: atlasCompanyId, - }); - await ensureUser({ - name: "Aline Rezende", - email: "aline.rezende@atlasengenharia.com.br", - role: "MANAGER", - companyId: atlasCompanyId, - }); - - const omniManagerId = await ensureUser({ - name: "Fernanda Lima", - email: "fernanda.lima@omnisaude.com.br", - role: "MANAGER", - companyId: omniCompanyId, - }); - - const ricardoOmniId = await ensureUser({ - name: "Ricardo Matos", - email: "ricardo.matos@omnisaude.com.br", - role: "MANAGER", - companyId: omniCompanyId, - }); - await ensureUser({ - name: "Luciana Prado", - email: "luciana.prado@omnisaude.com.br", - role: "MANAGER", - companyId: omniCompanyId, - }); - const clienteDemoId = await ensureUser({ - name: "Cliente Demo", - email: "cliente.demo@sistema.dev", - role: "MANAGER", - companyId: omniCompanyId, - }); const templateDefinitions = [ { @@ -301,103 +152,6 @@ export const seedDemo = mutation({ }); } - // Seed a couple of tickets - const now = Date.now(); - const newestRef = await ctx.db - .query("tickets") - .withIndex("by_tenant_reference", (q) => q.eq("tenantId", tenantId)) - .order("desc") - .take(1); - let ref = newestRef[0]?.reference ?? 41000; - const queue1 = queueChamados._id; - const queue2 = queueLaboratorio._id; - - const t1 = await ctx.db.insert("tickets", { - tenantId, - reference: ++ref, - subject: "Erro 500 ao acessar portal do cliente", - summary: "Clientes relatam erro intermitente no portal web", - status: "AWAITING_ATTENDANCE", - priority: "URGENT", - channel: "EMAIL", - queueId: queue1, - requesterId: joaoAtlasId, - assigneeId: defaultAssigneeId, - companyId: atlasCompanyId, - createdAt: now - 1000 * 60 * 60 * 5, - updatedAt: now - 1000 * 60 * 10, - tags: ["portal", "cliente"], - }); - await ctx.db.insert("ticketEvents", { - ticketId: t1, - type: "CREATED", - createdAt: now - 1000 * 60 * 60 * 5, - payload: {}, - }); - await ctx.db.insert("ticketEvents", { - ticketId: t1, - type: "MANAGER_NOTIFIED", - createdAt: now - 1000 * 60 * 60 * 4, - payload: { managerUserId: atlasManagerId }, - }); - - const t2 = await ctx.db.insert("tickets", { - tenantId, - reference: ++ref, - subject: "Integração ERP parada", - summary: "Webhook do ERP retornando timeout", - status: "PENDING", - priority: "HIGH", - channel: "WHATSAPP", - queueId: queue2, - requesterId: ricardoOmniId, - assigneeId: defaultAssigneeId, - companyId: omniCompanyId, - createdAt: now - 1000 * 60 * 60 * 8, - updatedAt: now - 1000 * 60 * 30, - tags: ["Integração", "erp"], - }); - await ctx.db.insert("ticketEvents", { - ticketId: t2, - type: "CREATED", - createdAt: now - 1000 * 60 * 60 * 8, - payload: {}, - }); - await ctx.db.insert("ticketEvents", { - ticketId: t2, - type: "MANAGER_NOTIFIED", - createdAt: now - 1000 * 60 * 60 * 7, - payload: { managerUserId: omniManagerId }, - }); - - const t3 = await ctx.db.insert("tickets", { - tenantId, - reference: ++ref, - subject: "Visita técnica para instalação de roteadores", - summary: "Equipe Omni solicita agenda para instalação de novos pontos de rede", - status: "AWAITING_ATTENDANCE", - priority: "MEDIUM", - channel: "PHONE", - queueId: queueVisitas._id, - requesterId: clienteDemoId, - assigneeId: defaultAssigneeId, - companyId: omniCompanyId, - createdAt: now - 1000 * 60 * 60 * 3, - updatedAt: now - 1000 * 60 * 20, - tags: ["visita", "infraestrutura"], - }); - await ctx.db.insert("ticketEvents", { - ticketId: t3, - type: "CREATED", - createdAt: now - 1000 * 60 * 60 * 3, - payload: {}, - }); - await ctx.db.insert("ticketEvents", { - ticketId: t3, - type: "VISIT_SCHEDULED", - createdAt: now - 1000 * 60 * 15, - payload: { scheduledFor: now + 1000 * 60 * 60 * 24 }, - }); + // No demo tickets are seeded; rely on real data from the database. }, }); - const generateCode = () => Array.from(randomBytes(32), (b) => b.toString(16).padStart(2, "0")).join("") diff --git a/src/app/dashboard/data.json b/src/app/dashboard/data.json deleted file mode 100644 index fc432b0..0000000 --- a/src/app/dashboard/data.json +++ /dev/null @@ -1,614 +0,0 @@ -[ - { - "id": 1, - "header": "Cover page", - "type": "Cover page", - "status": "In Process", - "target": "18", - "limit": "5", - "reviewer": "Eddie Lake" - }, - { - "id": 2, - "header": "Table of contents", - "type": "Table of contents", - "status": "Done", - "target": "29", - "limit": "24", - "reviewer": "Eddie Lake" - }, - { - "id": 3, - "header": "Executive summary", - "type": "Narrative", - "status": "Done", - "target": "10", - "limit": "13", - "reviewer": "Eddie Lake" - }, - { - "id": 4, - "header": "Technical approach", - "type": "Narrative", - "status": "Done", - "target": "27", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 5, - "header": "Design", - "type": "Narrative", - "status": "In Process", - "target": "2", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 6, - "header": "Capabilities", - "type": "Narrative", - "status": "In Process", - "target": "20", - "limit": "8", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 7, - "header": "Integration with existing systems", - "type": "Narrative", - "status": "In Process", - "target": "19", - "limit": "21", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 8, - "header": "Innovation and Advantages", - "type": "Narrative", - "status": "Done", - "target": "25", - "limit": "26", - "reviewer": "Assign reviewer" - }, - { - "id": 9, - "header": "Overview of EMR's Innovative Solutions", - "type": "Technical content", - "status": "Done", - "target": "7", - "limit": "23", - "reviewer": "Assign reviewer" - }, - { - "id": 10, - "header": "Advanced Algorithms and Machine Learning", - "type": "Narrative", - "status": "Done", - "target": "30", - "limit": "28", - "reviewer": "Assign reviewer" - }, - { - "id": 11, - "header": "Adaptive Communication Protocols", - "type": "Narrative", - "status": "Done", - "target": "9", - "limit": "31", - "reviewer": "Assign reviewer" - }, - { - "id": 12, - "header": "Advantages Over Current Technologies", - "type": "Narrative", - "status": "Done", - "target": "12", - "limit": "0", - "reviewer": "Assign reviewer" - }, - { - "id": 13, - "header": "Past Performance", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "33", - "reviewer": "Assign reviewer" - }, - { - "id": 14, - "header": "Customer Feedback and Satisfaction Levels", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "34", - "reviewer": "Assign reviewer" - }, - { - "id": 15, - "header": "Implementation Challenges and Solutions", - "type": "Narrative", - "status": "Done", - "target": "3", - "limit": "35", - "reviewer": "Assign reviewer" - }, - { - "id": 16, - "header": "Security Measures and Data Protection Policies", - "type": "Narrative", - "status": "In Process", - "target": "6", - "limit": "36", - "reviewer": "Assign reviewer" - }, - { - "id": 17, - "header": "Scalability and Future Proofing", - "type": "Narrative", - "status": "Done", - "target": "4", - "limit": "37", - "reviewer": "Assign reviewer" - }, - { - "id": 18, - "header": "Cost-Benefit Analysis", - "type": "Plain language", - "status": "Done", - "target": "14", - "limit": "38", - "reviewer": "Assign reviewer" - }, - { - "id": 19, - "header": "User Training and Onboarding Experience", - "type": "Narrative", - "status": "Done", - "target": "17", - "limit": "39", - "reviewer": "Assign reviewer" - }, - { - "id": 20, - "header": "Future Development Roadmap", - "type": "Narrative", - "status": "Done", - "target": "11", - "limit": "40", - "reviewer": "Assign reviewer" - }, - { - "id": 21, - "header": "System Architecture Overview", - "type": "Technical content", - "status": "In Process", - "target": "24", - "limit": "18", - "reviewer": "Maya Johnson" - }, - { - "id": 22, - "header": "Risk Management Plan", - "type": "Narrative", - "status": "Done", - "target": "15", - "limit": "22", - "reviewer": "Carlos Rodriguez" - }, - { - "id": 23, - "header": "Compliance Documentation", - "type": "Legal", - "status": "In Process", - "target": "31", - "limit": "27", - "reviewer": "Sarah Chen" - }, - { - "id": 24, - "header": "API Documentation", - "type": "Technical content", - "status": "Done", - "target": "8", - "limit": "12", - "reviewer": "Raj Patel" - }, - { - "id": 25, - "header": "User Interface Mockups", - "type": "Visual", - "status": "In Process", - "target": "19", - "limit": "25", - "reviewer": "Leila Ahmadi" - }, - { - "id": 26, - "header": "Database Schema", - "type": "Technical content", - "status": "Done", - "target": "22", - "limit": "20", - "reviewer": "Thomas Wilson" - }, - { - "id": 27, - "header": "Testing Methodology", - "type": "Technical content", - "status": "In Process", - "target": "17", - "limit": "14", - "reviewer": "Assign reviewer" - }, - { - "id": 28, - "header": "Deployment Strategy", - "type": "Narrative", - "status": "Done", - "target": "26", - "limit": "30", - "reviewer": "Eddie Lake" - }, - { - "id": 29, - "header": "Budget Breakdown", - "type": "Financial", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 30, - "header": "Market Analysis", - "type": "Research", - "status": "Done", - "target": "29", - "limit": "32", - "reviewer": "Sophia Martinez" - }, - { - "id": 31, - "header": "Competitor Comparison", - "type": "Research", - "status": "In Process", - "target": "21", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 32, - "header": "Maintenance Plan", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "23", - "reviewer": "Alex Thompson" - }, - { - "id": 33, - "header": "User Personas", - "type": "Research", - "status": "In Process", - "target": "27", - "limit": "24", - "reviewer": "Nina Patel" - }, - { - "id": 34, - "header": "Accessibility Compliance", - "type": "Legal", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 35, - "header": "Performance Metrics", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "David Kim" - }, - { - "id": 36, - "header": "Disaster Recovery Plan", - "type": "Technical content", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 37, - "header": "Third-party Integrations", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Eddie Lake" - }, - { - "id": 38, - "header": "User Feedback Summary", - "type": "Research", - "status": "Done", - "target": "20", - "limit": "15", - "reviewer": "Assign reviewer" - }, - { - "id": 39, - "header": "Localization Strategy", - "type": "Narrative", - "status": "In Process", - "target": "12", - "limit": "19", - "reviewer": "Maria Garcia" - }, - { - "id": 40, - "header": "Mobile Compatibility", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "James Wilson" - }, - { - "id": 41, - "header": "Data Migration Plan", - "type": "Technical content", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Assign reviewer" - }, - { - "id": 42, - "header": "Quality Assurance Protocols", - "type": "Technical content", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Priya Singh" - }, - { - "id": 43, - "header": "Stakeholder Analysis", - "type": "Research", - "status": "In Process", - "target": "11", - "limit": "14", - "reviewer": "Eddie Lake" - }, - { - "id": 44, - "header": "Environmental Impact Assessment", - "type": "Research", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Assign reviewer" - }, - { - "id": 45, - "header": "Intellectual Property Rights", - "type": "Legal", - "status": "In Process", - "target": "17", - "limit": "20", - "reviewer": "Sarah Johnson" - }, - { - "id": 46, - "header": "Customer Support Framework", - "type": "Narrative", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 47, - "header": "Version Control Strategy", - "type": "Technical content", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 48, - "header": "Continuous Integration Pipeline", - "type": "Technical content", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Michael Chen" - }, - { - "id": 49, - "header": "Regulatory Compliance", - "type": "Legal", - "status": "In Process", - "target": "13", - "limit": "16", - "reviewer": "Assign reviewer" - }, - { - "id": 50, - "header": "User Authentication System", - "type": "Technical content", - "status": "Done", - "target": "28", - "limit": "31", - "reviewer": "Eddie Lake" - }, - { - "id": 51, - "header": "Data Analytics Framework", - "type": "Technical content", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 52, - "header": "Cloud Infrastructure", - "type": "Technical content", - "status": "Done", - "target": "16", - "limit": "19", - "reviewer": "Assign reviewer" - }, - { - "id": 53, - "header": "Network Security Measures", - "type": "Technical content", - "status": "In Process", - "target": "29", - "limit": "32", - "reviewer": "Lisa Wong" - }, - { - "id": 54, - "header": "Project Timeline", - "type": "Planning", - "status": "Done", - "target": "14", - "limit": "17", - "reviewer": "Eddie Lake" - }, - { - "id": 55, - "header": "Resource Allocation", - "type": "Planning", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Assign reviewer" - }, - { - "id": 56, - "header": "Team Structure and Roles", - "type": "Planning", - "status": "Done", - "target": "20", - "limit": "23", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 57, - "header": "Communication Protocols", - "type": "Planning", - "status": "In Process", - "target": "15", - "limit": "18", - "reviewer": "Assign reviewer" - }, - { - "id": 58, - "header": "Success Metrics", - "type": "Planning", - "status": "Done", - "target": "30", - "limit": "33", - "reviewer": "Eddie Lake" - }, - { - "id": 59, - "header": "Internationalization Support", - "type": "Technical content", - "status": "In Process", - "target": "23", - "limit": "26", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 60, - "header": "Backup and Recovery Procedures", - "type": "Technical content", - "status": "Done", - "target": "18", - "limit": "21", - "reviewer": "Assign reviewer" - }, - { - "id": 61, - "header": "Monitoring and Alerting System", - "type": "Technical content", - "status": "In Process", - "target": "25", - "limit": "28", - "reviewer": "Daniel Park" - }, - { - "id": 62, - "header": "Code Review Guidelines", - "type": "Technical content", - "status": "Done", - "target": "12", - "limit": "15", - "reviewer": "Eddie Lake" - }, - { - "id": 63, - "header": "Documentation Standards", - "type": "Technical content", - "status": "In Process", - "target": "27", - "limit": "30", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 64, - "header": "Release Management Process", - "type": "Planning", - "status": "Done", - "target": "22", - "limit": "25", - "reviewer": "Assign reviewer" - }, - { - "id": 65, - "header": "Feature Prioritization Matrix", - "type": "Planning", - "status": "In Process", - "target": "19", - "limit": "22", - "reviewer": "Emma Davis" - }, - { - "id": 66, - "header": "Technical Debt Assessment", - "type": "Technical content", - "status": "Done", - "target": "24", - "limit": "27", - "reviewer": "Eddie Lake" - }, - { - "id": 67, - "header": "Capacity Planning", - "type": "Planning", - "status": "In Process", - "target": "21", - "limit": "24", - "reviewer": "Jamik Tashpulatov" - }, - { - "id": 68, - "header": "Service Level Agreements", - "type": "Legal", - "status": "Done", - "target": "26", - "limit": "29", - "reviewer": "Assign reviewer" - } -] diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 679b1e8..c5a51e2 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -16,12 +16,7 @@ export default async function Dashboard() { - - - } + primaryAction={} /> } > diff --git a/src/app/play/page.tsx b/src/app/play/page.tsx index d9ae47e..4a6e001 100644 --- a/src/app/play/page.tsx +++ b/src/app/play/page.tsx @@ -7,20 +7,20 @@ import { requireAuthenticatedSession } from "@/lib/auth-server" export default async function PlayPage() { await requireAuthenticatedSession() return ( - Pausar notificacoes} - primaryAction={Iniciar sessao} - /> - } - > -
- - -
-
- ) -} + Pausar notificações} + primaryAction={Iniciar sessão} + /> + } + > +
+ + +
+
+ ) +} diff --git a/src/app/tickets/[id]/page.tsx b/src/app/tickets/[id]/page.tsx index b9b5b93..d5e69c4 100644 --- a/src/app/tickets/[id]/page.tsx +++ b/src/app/tickets/[id]/page.tsx @@ -1,10 +1,7 @@ import { AppShell } from "@/components/app-shell" import { SiteHeader } from "@/components/site-header" import { TicketDetailView } from "@/components/tickets/ticket-detail-view" -import { TicketDetailStatic } from "@/components/tickets/ticket-detail-static" import { NewTicketDialogDeferred } from "@/components/tickets/new-ticket-dialog.client" -import { getTicketById } from "@/lib/mocks/tickets" -import type { TicketWithDetails } from "@/lib/schemas/ticket" import { requireAuthenticatedSession } from "@/lib/auth-server" type TicketDetailPageProps = { @@ -14,8 +11,6 @@ type TicketDetailPageProps = { export default async function TicketDetailPage({ params }: TicketDetailPageProps) { await requireAuthenticatedSession() const { id } = await params - const isMock = id.startsWith("ticket-") - const mock = isMock ? getTicketById(id) : null return ( } > - {isMock && mock ? : } + ) } diff --git a/src/app/tickets/tickets-page-client.tsx b/src/app/tickets/tickets-page-client.tsx index a2e0927..713fbe1 100644 --- a/src/app/tickets/tickets-page-client.tsx +++ b/src/app/tickets/tickets-page-client.tsx @@ -36,8 +36,8 @@ export function TicketsPageClient() { Exportar CSV} primaryAction={} + secondaryAction={Exportar CSV} /> } > diff --git a/src/components/admin/admin-users-manager.tsx b/src/components/admin/admin-users-manager.tsx index 7d446f2..9033a2d 100644 --- a/src/components/admin/admin-users-manager.tsx +++ b/src/components/admin/admin-users-manager.tsx @@ -2,6 +2,7 @@ import Link from "next/link" import { useCallback, useEffect, useMemo, useState, useTransition } from "react" +import { IconSearch, IconUserPlus } from "@tabler/icons-react" import { toast } from "sonner" @@ -215,6 +216,50 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d const teamUsers = useMemo(() => users.filter((user) => user.role !== "machine"), [users]) const machineUsers = useMemo(() => users.filter((user) => user.role === "machine"), [users]) + const defaultCreateRole = (selectableRoles.find((role) => role !== "machine") ?? "agent") as RoleOption + const [teamSearch, setTeamSearch] = useState("") + const [teamRoleFilter, setTeamRoleFilter] = useState<"all" | RoleOption>("all") + const [machineSearch, setMachineSearch] = useState("") + const [createDialogOpen, setCreateDialogOpen] = useState(false) + const [createForm, setCreateForm] = useState({ + name: "", + email: "", + role: defaultCreateRole, + tenantId: defaultTenantId, + companyId: NO_COMPANY_ID, + }) + const [isCreatingUser, setIsCreatingUser] = useState(false) + const [createPassword, setCreatePassword] = useState(null) + + const filteredTeamUsers = useMemo(() => { + const term = teamSearch.trim().toLowerCase() + return teamUsers.filter((user) => { + if (teamRoleFilter !== "all" && coerceRole(user.role) !== teamRoleFilter) return false + if (!term) return true + const haystack = [ + user.name ?? "", + user.email ?? "", + user.companyName ?? "", + formatRole(user.role), + ] + .join(" ") + .toLowerCase() + return haystack.includes(term) + }) + }, [teamUsers, teamSearch, teamRoleFilter]) + + const filteredMachineUsers = useMemo(() => { + const term = machineSearch.trim().toLowerCase() + if (!term) return machineUsers + return machineUsers.filter((user) => { + return ( + (user.name ?? "").toLowerCase().includes(term) || + user.email.toLowerCase().includes(term) || + (user.machinePersona ?? "").toLowerCase().includes(term) + ) + }) + }, [machineUsers, machineSearch]) + useEffect(() => { void (async () => { try { @@ -248,6 +293,14 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d }) setPasswordPreview(null) }, [editUser, defaultTenantId]) + useEffect(() => { + setCreateForm((prev) => ({ + ...prev, + role: defaultCreateRole, + tenantId: defaultTenantId, + })) + }, [defaultCreateRole, defaultTenantId]) + async function handleInviteSubmit(event: React.FormEvent) { event.preventDefault() @@ -405,6 +458,115 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d } } + const resetCreateForm = useCallback(() => { + setCreateForm({ + name: "", + email: "", + role: defaultCreateRole, + tenantId: defaultTenantId, + companyId: NO_COMPANY_ID, + }) + setCreatePassword(null) + }, [defaultCreateRole, defaultTenantId]) + + const handleOpenCreateUser = useCallback(() => { + resetCreateForm() + setCreateDialogOpen(true) + }, [resetCreateForm]) + + const handleCloseCreateDialog = useCallback(() => { + resetCreateForm() + setCreateDialogOpen(false) + }, [resetCreateForm]) + + async function handleCreateUser(event: React.FormEvent) { + event.preventDefault() + + const payload = { + name: createForm.name.trim(), + email: createForm.email.trim().toLowerCase(), + role: createForm.role, + tenantId: createForm.tenantId.trim() || defaultTenantId, + } + + if (!payload.name) { + toast.error("Informe o nome do usuário") + return + } + if (!payload.email || !payload.email.includes("@")) { + toast.error("Informe um e-mail válido") + return + } + + setIsCreatingUser(true) + setCreatePassword(null) + try { + const response = await fetch("/api/admin/users", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify(payload), + }) + + if (!response.ok) { + const data = await response.json().catch(() => ({})) + throw new Error(data.error ?? "Não foi possível criar o usuário") + } + + const data = (await response.json()) as { user: { id: string; email: string; name: string | null; role: string; tenantId: string | null; createdAt: string }; temporaryPassword: string } + const normalizedRole = coerceRole(data.user.role) + const normalizedUser: AdminUser = { + id: data.user.id, + email: data.user.email, + name: data.user.name ?? data.user.email, + role: normalizedRole, + tenantId: data.user.tenantId ?? defaultTenantId, + createdAt: data.user.createdAt, + updatedAt: null, + companyId: null, + companyName: null, + machinePersona: null, + } + + if (createForm.companyId && createForm.companyId !== NO_COMPANY_ID) { + try { + const assignResponse = await fetch("/api/admin/users/assign-company", { + method: "POST", + headers: { "Content-Type": "application/json" }, + credentials: "include", + body: JSON.stringify({ email: normalizedUser.email, companyId: createForm.companyId }), + }) + if (assignResponse.ok) { + const company = companies.find((company) => company.id === createForm.companyId) + normalizedUser.companyId = createForm.companyId + normalizedUser.companyName = company?.name ?? null + } else { + const assignData = await assignResponse.json().catch(() => ({})) + toast.error(assignData.error ?? "Não foi possível vincular a empresa") + } + } catch (error) { + const message = error instanceof Error ? error.message : "Falha ao vincular empresa" + toast.error(message) + } + } + + setUsers((previous) => [normalizedUser, ...previous]) + setCreatePassword(data.temporaryPassword) + toast.success("Usuário criado com sucesso") + setCreateForm((prev) => ({ + ...prev, + name: "", + email: "", + companyId: NO_COMPANY_ID, + })) + } catch (error) { + const message = error instanceof Error ? error.message : "Erro ao criar usuário" + toast.error(message) + } finally { + setIsCreatingUser(false) + } + } + async function handleSaveUser(event: React.FormEvent) { event.preventDefault() if (!editUser) return @@ -549,6 +711,56 @@ async function handleDeleteUser() { +
+
+

Equipe cadastrada

+

+ {filteredTeamUsers.length} {filteredTeamUsers.length === 1 ? "colaborador" : "colaboradores"} +

+
+ +
+
+
+ + setTeamSearch(event.target.value)} + placeholder="Buscar por nome, e-mail ou empresa..." + className="h-9 pl-9" + /> +
+
+ + {(teamSearch.trim().length > 0 || teamRoleFilter !== "all") ? ( + + ) : null} +
+
Equipe cadastrada @@ -568,7 +780,7 @@ async function handleDeleteUser() { - {teamUsers.map((user) => ( + {filteredTeamUsers.map((user) => ( {user.name || "—"} {user.email} @@ -590,9 +802,9 @@ async function handleDeleteUser() { Editar - - - - - ))} - {machineUsers.length === 0 ? ( + {filteredMachineUsers.map((user) => { + const machineId = extractMachineId(user.email) + return ( + + {user.name || "Máquina"} + {user.email} + {user.machinePersona ? user.machinePersona === "manager" ? "Gestor" : "Colaborador" : "—"} + {formatDate(user.createdAt)} + +
+ + + +
+ + + ) + })} + {filteredMachineUsers.length === 0 ? ( - Nenhuma máquina provisionada ainda. + {machineUsers.length === 0 + ? "Nenhuma máquina provisionada ainda." + : "Nenhum agente encontrado para a busca atual."} ) : null} @@ -751,7 +992,7 @@ async function handleDeleteUser() {
setCreateForm((prev) => ({ ...prev, name: event.target.value }))} + required + autoComplete="off" + /> +
+
+ + setCreateForm((prev) => ({ ...prev, email: event.target.value }))} + required + autoComplete="off" + /> +
+
+ + +
+
+ + setCreateForm((prev) => ({ ...prev, tenantId: event.target.value }))} + /> +
+
+ + +
+ {createPassword ? ( +
+

Senha temporária gerada:

+
+ {createPassword} + +
+
+ ) : null} + + + + + + + + (!open ? setEditUserId(null) : null)}> diff --git a/src/components/admin/clients/admin-clients-manager.tsx b/src/components/admin/clients/admin-clients-manager.tsx index c466f99..8a0adf2 100644 --- a/src/components/admin/clients/admin-clients-manager.tsx +++ b/src/components/admin/clients/admin-clients-manager.tsx @@ -235,9 +235,9 @@ export function AdminClientsManager({ initialClients }: { initialClients: AdminC enableSorting: false, cell: ({ row }) => (
- + {isMobile ? ( -
+
{hasCompanies ? ( filteredCompanies.map((company) => { const companyMachines = machinesByCompanyId.get(company.id) ?? [] @@ -554,6 +569,10 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies: Editar empresa + setMachinesDialog({ companyId: company.id, name: company.name })}> + + Ver máquinas + void toggleAvulso(company)}> {company.isAvulso ? "Marcar como recorrente" : "Marcar como avulso"} @@ -673,9 +692,9 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies: )}
) : ( -
+
- + Empresa Provisionamento @@ -935,23 +954,26 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies: } }} > - + - Excluir empresa - +
+ + Confirmar exclusão +
+ Esta operação remove o cadastro do cliente e impede novos vínculos automáticos.

- Confirme a exclusão de{" "} - {deleteTarget?.name ?? "empresa selecionada"}. + Deseja remover definitivamente{" "} + {deleteTarget?.name ?? "a empresa selecionada"}?

- Registros históricos que apontem para a empresa poderão impedir a exclusão. + Registros históricos podem impedir a exclusão. Usuários e tickets ainda vinculados serão desvinculados automaticamente.

- + diff --git a/src/components/admin/machines/admin-machines-overview.tsx b/src/components/admin/machines/admin-machines-overview.tsx index 1ce3a47..9e070fc 100644 --- a/src/components/admin/machines/admin-machines-overview.tsx +++ b/src/components/admin/machines/admin-machines-overview.tsx @@ -14,7 +14,7 @@ import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Checkbox } from "@/components/ui/checkbox" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" +import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog" import { Table, @@ -1310,6 +1310,23 @@ export function MachineDetails({ machine }: MachineDetailsProps) { Detalhes Resumo da máquina selecionada. + {machine ? ( + +
+ {companyName ? ( +
+ {companyName} +
+ ) : null} + + {!isActive ? ( + + Máquina desativada + + ) : null} +
+
+ ) : null}
{!machine ? ( @@ -1317,7 +1334,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { ) : (
-
+

{machine.hostname}

@@ -1330,19 +1347,6 @@ export function MachineDetails({ machine }: MachineDetailsProps) { {machine.authEmail ?? "E-mail não definido"}

-
- {companyName ? ( -
- {companyName} -
- ) : null} - - {!isActive ? ( - - Máquina desativada - - ) : null} -
{/* ping integrado na badge de status */}
@@ -1375,7 +1379,7 @@ export function MachineDetails({ machine }: MachineDetailsProps) { {isActive ? (togglingActive ? "Desativando..." : "Desativar") : togglingActive ? "Reativando..." : "Reativar"} {machine.registeredBy ? ( - + Registrada via {machine.registeredBy} ) : null} @@ -2667,14 +2671,14 @@ function MetricsGrid({ metrics, hardware, disks }: { metrics: MachineMetrics; ha const percentLabel = card.percent !== null ? `${Math.round(card.percent)}%` : "—" return (
-
+
-
+
{percentLabel}
diff --git a/src/components/portal/portal-ticket-detail.tsx b/src/components/portal/portal-ticket-detail.tsx index ef93070..a774dbf 100644 --- a/src/components/portal/portal-ticket-detail.tsx +++ b/src/components/portal/portal-ticket-detail.tsx @@ -494,11 +494,11 @@ export function PortalTicketDetail({ ticketId }: PortalTicketDetailProps) {
{ if (!open) setPreviewAttachment(null) }}> - - + + {previewAttachment?.name ?? "Visualização do anexo"} - + @@ -611,13 +611,22 @@ function PortalCommentAttachmentCard({ const target = await ensureUrl() if (!target) return try { + const response = await fetch(target, { credentials: "include" }) + if (!response.ok) { + throw new Error(`Unexpected status ${response.status}`) + } + const blob = await response.blob() + const blobUrl = window.URL.createObjectURL(blob) const link = document.createElement("a") - link.href = target + link.href = blobUrl link.download = attachment.name ?? "anexo" link.rel = "noopener noreferrer" document.body.appendChild(link) link.click() document.body.removeChild(link) + window.setTimeout(() => { + window.URL.revokeObjectURL(blobUrl) + }, 1000) } catch (error) { console.error("Falha ao iniciar download do anexo", error) window.open(target, "_blank", "noopener,noreferrer") diff --git a/src/components/section-cards.tsx b/src/components/section-cards.tsx index 808ef67..2ac0b09 100644 --- a/src/components/section-cards.tsx +++ b/src/components/section-cards.tsx @@ -118,7 +118,10 @@ export function SectionCards() { - Tempo médio da 1ª resposta + + Tempo médio da + 1ª resposta + {dashboard ? formatMinutes(dashboard.firstResponse.averageMinutes) : } @@ -169,7 +172,10 @@ export function SectionCards() { - Tickets resolvidos (7 dias) + + Tickets resolvidos + (7 dias) + {dashboard ? dashboard.resolution.resolvedLast7d : } diff --git a/src/components/tickets/play-next-ticket-card.tsx b/src/components/tickets/play-next-ticket-card.tsx index 7bdeed3..5f46453 100644 --- a/src/components/tickets/play-next-ticket-card.tsx +++ b/src/components/tickets/play-next-ticket-card.tsx @@ -116,7 +116,7 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {

{ticket.subject}

{ticket.summary}

-
+
{ticket.queue ?? "Sem fila"} Solicitante: {ticket.requester.name} @@ -163,4 +163,4 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) { ) -} +} diff --git a/src/components/tickets/ticket-comments.rich.tsx b/src/components/tickets/ticket-comments.rich.tsx index 9d6fbbb..23f2bc3 100644 --- a/src/components/tickets/ticket-comments.rich.tsx +++ b/src/components/tickets/ticket-comments.rich.tsx @@ -514,9 +514,9 @@ export function TicketComments({ ticket }: TicketCommentsProps) { !open && setPreview(null)}> - - Visualização do anexo - + + Visualização do anexo + @@ -614,13 +614,22 @@ function CommentAttachmentCard({ const target = url ?? (await ensureUrl()) if (!target) return try { + const response = await fetch(target, { credentials: "include" }) + if (!response.ok) { + throw new Error(`Unexpected status ${response.status}`) + } + const blob = await response.blob() + const blobUrl = window.URL.createObjectURL(blob) const link = document.createElement("a") - link.href = target + link.href = blobUrl link.download = attachment.name ?? "anexo" link.rel = "noopener noreferrer" document.body.appendChild(link) link.click() document.body.removeChild(link) + window.setTimeout(() => { + window.URL.revokeObjectURL(blobUrl) + }, 1000) } catch (error) { console.error("Failed to download attachment", error) window.open(target, "_blank", "noopener,noreferrer") diff --git a/src/components/tickets/ticket-detail-static.tsx b/src/components/tickets/ticket-detail-static.tsx deleted file mode 100644 index e5286d7..0000000 --- a/src/components/tickets/ticket-detail-static.tsx +++ /dev/null @@ -1,21 +0,0 @@ -"use client"; - -import { TicketSummaryHeader } from "@/components/tickets/ticket-summary-header"; -import { TicketDetailsPanel } from "@/components/tickets/ticket-details-panel"; -import { TicketTimeline } from "@/components/tickets/ticket-timeline"; -import type { TicketWithDetails } from "@/lib/schemas/ticket"; - -export function TicketDetailStatic({ ticket }: { ticket: TicketWithDetails }) { - return ( -
- -
-
- -
- -
-
- ); -} - diff --git a/src/components/tickets/ticket-detail-view.tsx b/src/components/tickets/ticket-detail-view.tsx index 5e9116e..ec2a46b 100644 --- a/src/components/tickets/ticket-detail-view.tsx +++ b/src/components/tickets/ticket-detail-view.tsx @@ -6,7 +6,6 @@ import { DEFAULT_TENANT_ID } from "@/lib/constants"; import { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket"; import type { Id } from "@/convex/_generated/dataModel"; import type { TicketWithDetails } from "@/lib/schemas/ticket"; -import { getTicketById } from "@/lib/mocks/tickets"; import { Card, CardContent } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; import { TicketComments } from "@/components/tickets/ticket-comments.rich"; @@ -16,9 +15,8 @@ import { TicketTimeline } from "@/components/tickets/ticket-timeline"; import { useAuth } from "@/lib/auth-client"; export function TicketDetailView({ id }: { id: string }) { - const isMockId = id.startsWith("ticket-"); const { convexUserId } = useAuth(); - const shouldSkip = isMockId || !convexUserId; + const shouldSkip = !convexUserId; const t = useQuery( api.tickets.getById, shouldSkip @@ -29,40 +27,66 @@ export function TicketDetailView({ id }: { id: string }) { viewerId: convexUserId as Id<"users">, } ); - let ticket: TicketWithDetails | null = null; - if (t) { - ticket = mapTicketWithDetailsFromServer(t as unknown); - } else if (isMockId) { - ticket = getTicketById(id) ?? null; - } - if (!ticket) return ( -
- - -
- - -
-
-
- - - {Array.from({ length: 3 }).map((_, i) => ( -
-
- -
- ))} -
-
+ const isLoading = shouldSkip || t === undefined; + + if (isLoading) { + return ( +
- {Array.from({ length: 5 }).map((_, i) => ())} +
+ + +
+
+
+ + + {Array.from({ length: 3 }).map((_, i) => ( +
+
+ +
+ ))} +
+
+ + + {Array.from({ length: 5 }).map((_, i) => ())} + + +
+
+ ); + } + + if (!t) { + return ( +
+ + +

Ticket não encontrado

+

O ticket solicitado não existe ou você não tem permissão para visualizá-lo.

-
- ); + ); + } + + const ticket = mapTicketWithDetailsFromServer(t as unknown) as TicketWithDetails | null; + + if (!ticket) { + return ( +
+ + +

Ticket não encontrado

+

O ticket solicitado não existe ou você não tem permissão para visualizá-lo.

+
+
+
+ ); + } return (
diff --git a/src/components/tickets/tickets-table.tsx b/src/components/tickets/tickets-table.tsx index e998915..a66df45 100644 --- a/src/components/tickets/tickets-table.tsx +++ b/src/components/tickets/tickets-table.tsx @@ -6,7 +6,6 @@ import { format, formatDistanceToNowStrict } from "date-fns" import { ptBR } from "date-fns/locale" import { type LucideIcon, Code, FileText, Mail, MessageCircle, MessageSquare, Phone } from "lucide-react" -import { tickets as ticketsMock } from "@/lib/mocks/tickets" import type { Ticket, TicketChannel, TicketStatus } from "@/lib/schemas/ticket" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" @@ -110,7 +109,8 @@ export type TicketsTableProps = { tickets?: Ticket[] } -export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) { +export function TicketsTable({ tickets }: TicketsTableProps) { + const safeTickets = tickets ?? [] const [now, setNow] = useState(() => Date.now()) const router = useRouter() @@ -152,7 +152,7 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) { Empresa - + Prioridade @@ -170,7 +170,7 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) { - {tickets.map((ticket) => { + {safeTickets.map((ticket) => { const ChannelIcon = channelIcon[ticket.channel] ?? MessageCircle return ( @@ -240,9 +240,9 @@ export function TicketsTable({ tickets = ticketsMock }: TicketsTableProps) { {((ticket.company ?? null) as { name?: string } | null)?.name ?? "—"} -
- {tickets.length === 0 && ( + {safeTickets.length === 0 && ( diff --git a/src/components/ui/phone-input.tsx b/src/components/ui/phone-input.tsx index 8a1eeda..a0e9d51 100644 --- a/src/components/ui/phone-input.tsx +++ b/src/components/ui/phone-input.tsx @@ -5,6 +5,7 @@ import { ChangeEvent, useEffect, useMemo, useState } from "react" import { cn } from "@/lib/utils" import { Input } from "@/components/ui/input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { COUNTRY_DIAL_CODES, type CountryDialCode } from "@/lib/country-codes" export type CountryOption = { code: string @@ -15,26 +16,7 @@ export type CountryOption = { formatLocal: (digits: string) => string } -const COUNTRY_OPTIONS: CountryOption[] = [ - { - code: "BR", - label: "Brasil", - dialCode: "+55", - flag: "🇧🇷", - maxDigits: 11, - formatLocal: formatBrazilPhone, - }, - { - code: "US", - label: "Estados Unidos", - dialCode: "+1", - flag: "🇺🇸", - maxDigits: 10, - formatLocal: formatUsPhone, - }, -] - -const DEFAULT_COUNTRY = COUNTRY_OPTIONS[0] +const DEFAULT_COUNTRY_CODE = "BR" function formatBrazilPhone(digits: string): string { const cleaned = digits.slice(0, 11) @@ -63,12 +45,18 @@ function extractDigits(value?: string | null): string { function findCountryByValue(value?: string | null): CountryOption { const digits = extractDigits(value) - return ( - COUNTRY_OPTIONS.find((option) => { - const dialDigits = extractDigits(option.dialCode) - return digits.startsWith(dialDigits) && digits.length > dialDigits.length - }) ?? DEFAULT_COUNTRY - ) + const dialDigits = extractDigits(DEFAULT_COUNTRY.dialCode) + let matched: CountryOption = DEFAULT_COUNTRY + let matchedLength = digits.startsWith(dialDigits) ? dialDigits.length : 0 + for (const option of COUNTRY_OPTIONS) { + const optionDialDigits = extractDigits(option.dialCode) + if (optionDialDigits.length === 0) continue + if (digits.startsWith(optionDialDigits) && optionDialDigits.length > matchedLength) { + matched = option + matchedLength = optionDialDigits.length + } + } + return matched } function toLocalDigits(value: string | null | undefined, country: CountryOption): string { @@ -88,9 +76,49 @@ function formatStoredValue(country: CountryOption, localDigits: string): string function placeholderFor(country: CountryOption): string { if (country.code === "BR") return "(11) 91234-5678" if (country.code === "US") return "(555) 123-4567" + if (country.code === "CA") return "(416) 123-4567" return "Número de telefone" } +function formatGenericPhone(digits: string): string { + const cleaned = digits.slice(0, 15) + if (!cleaned) return "" + return cleaned.replace(/(\d{3,4})(?=\d)/g, "$1 ").trim() +} + +function flagEmojiFor(code: string): string { + if (!code || code.length !== 2) return "🏳️" + const base = 127397 + const chars = Array.from(code.toUpperCase()).map((char) => base + char.charCodeAt(0)) + return String.fromCodePoint(...chars) +} + +const regionDisplayNames = + typeof Intl !== "undefined" && "DisplayNames" in Intl + ? new Intl.DisplayNames(["pt-BR", "pt", "en"], { type: "region" }) + : null + +const SPECIAL_FORMATS: Record string }> = { + BR: { maxDigits: 11, formatLocal: formatBrazilPhone }, + US: { maxDigits: 10, formatLocal: formatUsPhone }, + CA: { maxDigits: 10, formatLocal: formatUsPhone }, +} + +const COUNTRY_OPTIONS: CountryOption[] = COUNTRY_DIAL_CODES.map((entry: CountryDialCode) => { + const special = SPECIAL_FORMATS[entry.code] + return { + code: entry.code, + label: regionDisplayNames?.of(entry.code) ?? entry.name ?? entry.code, + dialCode: entry.dialCode, + flag: flagEmojiFor(entry.code), + maxDigits: special?.maxDigits ?? 15, + formatLocal: special?.formatLocal ?? formatGenericPhone, + } +}).sort((a, b) => a.label.localeCompare(b.label, "pt-BR")) + +const DEFAULT_COUNTRY = + COUNTRY_OPTIONS.find((option) => option.code === DEFAULT_COUNTRY_CODE) ?? COUNTRY_OPTIONS[0] + export function PhoneInput({ value, onChange, className, id, name }: PhoneInputProps) { const [selectedCountry, setSelectedCountry] = useState(DEFAULT_COUNTRY) const [localDigits, setLocalDigits] = useState("") @@ -131,7 +159,7 @@ export function PhoneInput({ value, onChange, className, id, name }: PhoneInputP - + {COUNTRY_OPTIONS.map((option) => ( diff --git a/src/lib/country-codes.ts b/src/lib/country-codes.ts new file mode 100644 index 0000000..e04a835 --- /dev/null +++ b/src/lib/country-codes.ts @@ -0,0 +1,1234 @@ +export const COUNTRY_DIAL_CODES = [ + { + code: "AF", + name: "Afghanistan", + dialCode: "+93", + }, + { + code: "AX", + name: "Åland Islands", + dialCode: "+35818", + }, + { + code: "AL", + name: "Albania", + dialCode: "+355", + }, + { + code: "DZ", + name: "Algeria", + dialCode: "+213", + }, + { + code: "AS", + name: "American Samoa", + dialCode: "+1684", + }, + { + code: "AD", + name: "Andorra", + dialCode: "+376", + }, + { + code: "AO", + name: "Angola", + dialCode: "+244", + }, + { + code: "AI", + name: "Anguilla", + dialCode: "+1264", + }, + { + code: "AG", + name: "Antigua and Barbuda", + dialCode: "+1268", + }, + { + code: "AR", + name: "Argentina", + dialCode: "+54", + }, + { + code: "AM", + name: "Armenia", + dialCode: "+374", + }, + { + code: "AW", + name: "Aruba", + dialCode: "+297", + }, + { + code: "AU", + name: "Australia", + dialCode: "+61", + }, + { + code: "AT", + name: "Austria", + dialCode: "+43", + }, + { + code: "AZ", + name: "Azerbaijan", + dialCode: "+994", + }, + { + code: "BS", + name: "Bahamas", + dialCode: "+1242", + }, + { + code: "BH", + name: "Bahrain", + dialCode: "+973", + }, + { + code: "BD", + name: "Bangladesh", + dialCode: "+880", + }, + { + code: "BB", + name: "Barbados", + dialCode: "+1246", + }, + { + code: "BY", + name: "Belarus", + dialCode: "+375", + }, + { + code: "BE", + name: "Belgium", + dialCode: "+32", + }, + { + code: "BZ", + name: "Belize", + dialCode: "+501", + }, + { + code: "BJ", + name: "Benin", + dialCode: "+229", + }, + { + code: "BM", + name: "Bermuda", + dialCode: "+1441", + }, + { + code: "BT", + name: "Bhutan", + dialCode: "+975", + }, + { + code: "BO", + name: "Bolivia", + dialCode: "+591", + }, + { + code: "BQ", + name: "Bonaire, Sint Eustatius and Saba", + dialCode: "+5997", + }, + { + code: "BA", + name: "Bosnia and Herzegovina", + dialCode: "+387", + }, + { + code: "BW", + name: "Botswana", + dialCode: "+267", + }, + { + code: "BR", + name: "Brazil", + dialCode: "+55", + }, + { + code: "IO", + name: "British Indian Ocean Territory", + dialCode: "+246", + }, + { + code: "VG", + name: "British Virgin Islands", + dialCode: "+1284", + }, + { + code: "BN", + name: "Brunei", + dialCode: "+673", + }, + { + code: "BG", + name: "Bulgaria", + dialCode: "+359", + }, + { + code: "BF", + name: "Burkina Faso", + dialCode: "+226", + }, + { + code: "BI", + name: "Burundi", + dialCode: "+257", + }, + { + code: "KH", + name: "Cambodia", + dialCode: "+855", + }, + { + code: "CM", + name: "Cameroon", + dialCode: "+237", + }, + { + code: "CA", + name: "Canada", + dialCode: "+1", + }, + { + code: "CV", + name: "Cape Verde", + dialCode: "+238", + }, + { + code: "KY", + name: "Cayman Islands", + dialCode: "+1345", + }, + { + code: "CF", + name: "Central African Republic", + dialCode: "+236", + }, + { + code: "TD", + name: "Chad", + dialCode: "+235", + }, + { + code: "CL", + name: "Chile", + dialCode: "+56", + }, + { + code: "CN", + name: "China", + dialCode: "+86", + }, + { + code: "CX", + name: "Christmas Island", + dialCode: "+61", + }, + { + code: "CC", + name: "Cocos (Keeling) Islands", + dialCode: "+61", + }, + { + code: "CO", + name: "Colombia", + dialCode: "+57", + }, + { + code: "KM", + name: "Comoros", + dialCode: "+269", + }, + { + code: "CG", + name: "Congo", + dialCode: "+242", + }, + { + code: "CD", + name: "Congo (DRC)", + dialCode: "+243", + }, + { + code: "CK", + name: "Cook Islands", + dialCode: "+682", + }, + { + code: "CR", + name: "Costa Rica", + dialCode: "+506", + }, + { + code: "CI", + name: "Côte d’Ivoire", + dialCode: "+225", + }, + { + code: "HR", + name: "Croatia", + dialCode: "+385", + }, + { + code: "CU", + name: "Cuba", + dialCode: "+53", + }, + { + code: "CW", + name: "Curaçao", + dialCode: "+5999", + }, + { + code: "CY", + name: "Cyprus", + dialCode: "+357", + }, + { + code: "CZ", + name: "Czechia", + dialCode: "+420", + }, + { + code: "DK", + name: "Denmark", + dialCode: "+45", + }, + { + code: "DJ", + name: "Djibouti", + dialCode: "+253", + }, + { + code: "DM", + name: "Dominica", + dialCode: "+1767", + }, + { + code: "DO", + name: "Dominican Republic", + dialCode: "+1809", + }, + { + code: "EC", + name: "Ecuador", + dialCode: "+593", + }, + { + code: "EG", + name: "Egypt", + dialCode: "+20", + }, + { + code: "SV", + name: "El Salvador", + dialCode: "+503", + }, + { + code: "GQ", + name: "Equatorial Guinea", + dialCode: "+240", + }, + { + code: "ER", + name: "Eritrea", + dialCode: "+291", + }, + { + code: "EE", + name: "Estonia", + dialCode: "+372", + }, + { + code: "SZ", + name: "Eswatini", + dialCode: "+268", + }, + { + code: "ET", + name: "Ethiopia", + dialCode: "+251", + }, + { + code: "FK", + name: "Falkland Islands", + dialCode: "+500", + }, + { + code: "FO", + name: "Faroe Islands", + dialCode: "+298", + }, + { + code: "FJ", + name: "Fiji", + dialCode: "+679", + }, + { + code: "FI", + name: "Finland", + dialCode: "+358", + }, + { + code: "FR", + name: "France", + dialCode: "+33", + }, + { + code: "GF", + name: "French Guiana", + dialCode: "+594", + }, + { + code: "PF", + name: "French Polynesia", + dialCode: "+689", + }, + { + code: "TF", + name: "French Southern Territories", + dialCode: "+262", + }, + { + code: "GA", + name: "Gabon", + dialCode: "+241", + }, + { + code: "GM", + name: "Gambia", + dialCode: "+220", + }, + { + code: "GE", + name: "Georgia", + dialCode: "+995", + }, + { + code: "DE", + name: "Germany", + dialCode: "+49", + }, + { + code: "GH", + name: "Ghana", + dialCode: "+233", + }, + { + code: "GI", + name: "Gibraltar", + dialCode: "+350", + }, + { + code: "GR", + name: "Greece", + dialCode: "+30", + }, + { + code: "GL", + name: "Greenland", + dialCode: "+299", + }, + { + code: "GD", + name: "Grenada", + dialCode: "+1473", + }, + { + code: "GP", + name: "Guadeloupe", + dialCode: "+590", + }, + { + code: "GU", + name: "Guam", + dialCode: "+1671", + }, + { + code: "GT", + name: "Guatemala", + dialCode: "+502", + }, + { + code: "GG", + name: "Guernsey", + dialCode: "+44", + }, + { + code: "GN", + name: "Guinea", + dialCode: "+224", + }, + { + code: "GW", + name: "Guinea-Bissau", + dialCode: "+245", + }, + { + code: "GY", + name: "Guyana", + dialCode: "+592", + }, + { + code: "HT", + name: "Haiti", + dialCode: "+509", + }, + { + code: "HN", + name: "Honduras", + dialCode: "+504", + }, + { + code: "HK", + name: "Hong Kong", + dialCode: "+852", + }, + { + code: "HU", + name: "Hungary", + dialCode: "+36", + }, + { + code: "IS", + name: "Iceland", + dialCode: "+354", + }, + { + code: "IN", + name: "India", + dialCode: "+91", + }, + { + code: "ID", + name: "Indonesia", + dialCode: "+62", + }, + { + code: "IR", + name: "Iran", + dialCode: "+98", + }, + { + code: "IQ", + name: "Iraq", + dialCode: "+964", + }, + { + code: "IE", + name: "Ireland", + dialCode: "+353", + }, + { + code: "IM", + name: "Isle of Man", + dialCode: "+44", + }, + { + code: "IL", + name: "Israel", + dialCode: "+972", + }, + { + code: "IT", + name: "Italy", + dialCode: "+39", + }, + { + code: "JM", + name: "Jamaica", + dialCode: "+1876", + }, + { + code: "JP", + name: "Japan", + dialCode: "+81", + }, + { + code: "JE", + name: "Jersey", + dialCode: "+44", + }, + { + code: "JO", + name: "Jordan", + dialCode: "+962", + }, + { + code: "KZ", + name: "Kazakhstan", + dialCode: "+7", + }, + { + code: "KE", + name: "Kenya", + dialCode: "+254", + }, + { + code: "KI", + name: "Kiribati", + dialCode: "+686", + }, + { + code: "XK", + name: "Kosovo", + dialCode: "+383", + }, + { + code: "KW", + name: "Kuwait", + dialCode: "+965", + }, + { + code: "KG", + name: "Kyrgyzstan", + dialCode: "+996", + }, + { + code: "LA", + name: "Laos", + dialCode: "+856", + }, + { + code: "LV", + name: "Latvia", + dialCode: "+371", + }, + { + code: "LB", + name: "Lebanon", + dialCode: "+961", + }, + { + code: "LS", + name: "Lesotho", + dialCode: "+266", + }, + { + code: "LR", + name: "Liberia", + dialCode: "+231", + }, + { + code: "LY", + name: "Libya", + dialCode: "+218", + }, + { + code: "LI", + name: "Liechtenstein", + dialCode: "+423", + }, + { + code: "LT", + name: "Lithuania", + dialCode: "+370", + }, + { + code: "LU", + name: "Luxembourg", + dialCode: "+352", + }, + { + code: "MO", + name: "Macau", + dialCode: "+853", + }, + { + code: "MG", + name: "Madagascar", + dialCode: "+261", + }, + { + code: "MW", + name: "Malawi", + dialCode: "+265", + }, + { + code: "MY", + name: "Malaysia", + dialCode: "+60", + }, + { + code: "MV", + name: "Maldives", + dialCode: "+960", + }, + { + code: "ML", + name: "Mali", + dialCode: "+223", + }, + { + code: "MT", + name: "Malta", + dialCode: "+356", + }, + { + code: "MH", + name: "Marshall Islands", + dialCode: "+692", + }, + { + code: "MQ", + name: "Martinique", + dialCode: "+596", + }, + { + code: "MR", + name: "Mauritania", + dialCode: "+222", + }, + { + code: "MU", + name: "Mauritius", + dialCode: "+230", + }, + { + code: "YT", + name: "Mayotte", + dialCode: "+262", + }, + { + code: "MX", + name: "Mexico", + dialCode: "+52", + }, + { + code: "FM", + name: "Micronesia", + dialCode: "+691", + }, + { + code: "MD", + name: "Moldova", + dialCode: "+373", + }, + { + code: "MC", + name: "Monaco", + dialCode: "+377", + }, + { + code: "MN", + name: "Mongolia", + dialCode: "+976", + }, + { + code: "ME", + name: "Montenegro", + dialCode: "+382", + }, + { + code: "MS", + name: "Montserrat", + dialCode: "+1664", + }, + { + code: "MA", + name: "Morocco", + dialCode: "+212", + }, + { + code: "MZ", + name: "Mozambique", + dialCode: "+258", + }, + { + code: "MM", + name: "Myanmar", + dialCode: "+95", + }, + { + code: "NA", + name: "Namibia", + dialCode: "+264", + }, + { + code: "NR", + name: "Nauru", + dialCode: "+674", + }, + { + code: "NP", + name: "Nepal", + dialCode: "+977", + }, + { + code: "NL", + name: "Netherlands", + dialCode: "+31", + }, + { + code: "NC", + name: "New Caledonia", + dialCode: "+687", + }, + { + code: "NZ", + name: "New Zealand", + dialCode: "+64", + }, + { + code: "NI", + name: "Nicaragua", + dialCode: "+505", + }, + { + code: "NE", + name: "Niger", + dialCode: "+227", + }, + { + code: "NG", + name: "Nigeria", + dialCode: "+234", + }, + { + code: "NU", + name: "Niue", + dialCode: "+683", + }, + { + code: "NF", + name: "Norfolk Island", + dialCode: "+6723", + }, + { + code: "KP", + name: "North Korea", + dialCode: "+850", + }, + { + code: "MK", + name: "North Macedonia", + dialCode: "+389", + }, + { + code: "MP", + name: "Northern Mariana Islands", + dialCode: "+1670", + }, + { + code: "NO", + name: "Norway", + dialCode: "+47", + }, + { + code: "OM", + name: "Oman", + dialCode: "+968", + }, + { + code: "PK", + name: "Pakistan", + dialCode: "+92", + }, + { + code: "PW", + name: "Palau", + dialCode: "+680", + }, + { + code: "PS", + name: "Palestine", + dialCode: "+970", + }, + { + code: "PA", + name: "Panama", + dialCode: "+507", + }, + { + code: "PG", + name: "Papua New Guinea", + dialCode: "+675", + }, + { + code: "PY", + name: "Paraguay", + dialCode: "+595", + }, + { + code: "PE", + name: "Peru", + dialCode: "+51", + }, + { + code: "PH", + name: "Philippines", + dialCode: "+63", + }, + { + code: "PN", + name: "Pitcairn Islands", + dialCode: "+64", + }, + { + code: "PL", + name: "Poland", + dialCode: "+48", + }, + { + code: "PT", + name: "Portugal", + dialCode: "+351", + }, + { + code: "PR", + name: "Puerto Rico", + dialCode: "+1787", + }, + { + code: "QA", + name: "Qatar", + dialCode: "+974", + }, + { + code: "RE", + name: "Réunion", + dialCode: "+262", + }, + { + code: "RO", + name: "Romania", + dialCode: "+40", + }, + { + code: "RU", + name: "Russia", + dialCode: "+7", + }, + { + code: "RW", + name: "Rwanda", + dialCode: "+250", + }, + { + code: "BL", + name: "Saint Barthélemy", + dialCode: "+590", + }, + { + code: "SH", + name: "Saint Helena, Ascension and Tristan da Cunha", + dialCode: "+290", + }, + { + code: "KN", + name: "Saint Kitts and Nevis", + dialCode: "+1869", + }, + { + code: "LC", + name: "Saint Lucia", + dialCode: "+1758", + }, + { + code: "MF", + name: "Saint Martin", + dialCode: "+590", + }, + { + code: "PM", + name: "Saint Pierre and Miquelon", + dialCode: "+508", + }, + { + code: "VC", + name: "Saint Vincent and the Grenadines", + dialCode: "+1784", + }, + { + code: "WS", + name: "Samoa", + dialCode: "+685", + }, + { + code: "SM", + name: "San Marino", + dialCode: "+378", + }, + { + code: "ST", + name: "São Tomé and Príncipe", + dialCode: "+239", + }, + { + code: "SA", + name: "Saudi Arabia", + dialCode: "+966", + }, + { + code: "SN", + name: "Senegal", + dialCode: "+221", + }, + { + code: "RS", + name: "Serbia", + dialCode: "+381", + }, + { + code: "SC", + name: "Seychelles", + dialCode: "+248", + }, + { + code: "SL", + name: "Sierra Leone", + dialCode: "+232", + }, + { + code: "SG", + name: "Singapore", + dialCode: "+65", + }, + { + code: "SX", + name: "Sint Maarten", + dialCode: "+1721", + }, + { + code: "SK", + name: "Slovakia", + dialCode: "+421", + }, + { + code: "SI", + name: "Slovenia", + dialCode: "+386", + }, + { + code: "SB", + name: "Solomon Islands", + dialCode: "+677", + }, + { + code: "SO", + name: "Somalia", + dialCode: "+252", + }, + { + code: "ZA", + name: "South Africa", + dialCode: "+27", + }, + { + code: "GS", + name: "South Georgia", + dialCode: "+500", + }, + { + code: "KR", + name: "South Korea", + dialCode: "+82", + }, + { + code: "SS", + name: "South Sudan", + dialCode: "+211", + }, + { + code: "ES", + name: "Spain", + dialCode: "+34", + }, + { + code: "LK", + name: "Sri Lanka", + dialCode: "+94", + }, + { + code: "SD", + name: "Sudan", + dialCode: "+249", + }, + { + code: "SR", + name: "Suriname", + dialCode: "+597", + }, + { + code: "SE", + name: "Sweden", + dialCode: "+46", + }, + { + code: "CH", + name: "Switzerland", + dialCode: "+41", + }, + { + code: "SY", + name: "Syria", + dialCode: "+963", + }, + { + code: "TW", + name: "Taiwan", + dialCode: "+886", + }, + { + code: "TJ", + name: "Tajikistan", + dialCode: "+992", + }, + { + code: "TZ", + name: "Tanzania", + dialCode: "+255", + }, + { + code: "TH", + name: "Thailand", + dialCode: "+66", + }, + { + code: "TL", + name: "Timor-Leste", + dialCode: "+670", + }, + { + code: "TG", + name: "Togo", + dialCode: "+228", + }, + { + code: "TK", + name: "Tokelau", + dialCode: "+690", + }, + { + code: "TO", + name: "Tonga", + dialCode: "+676", + }, + { + code: "TT", + name: "Trinidad and Tobago", + dialCode: "+1868", + }, + { + code: "TN", + name: "Tunisia", + dialCode: "+216", + }, + { + code: "TR", + name: "Turkey", + dialCode: "+90", + }, + { + code: "TM", + name: "Turkmenistan", + dialCode: "+993", + }, + { + code: "TC", + name: "Turks and Caicos Islands", + dialCode: "+1649", + }, + { + code: "TV", + name: "Tuvalu", + dialCode: "+688", + }, + { + code: "UG", + name: "Uganda", + dialCode: "+256", + }, + { + code: "UA", + name: "Ukraine", + dialCode: "+380", + }, + { + code: "AE", + name: "United Arab Emirates", + dialCode: "+971", + }, + { + code: "GB", + name: "United Kingdom", + dialCode: "+44", + }, + { + code: "US", + name: "United States", + dialCode: "+1", + }, + { + code: "UM", + name: "United States Minor Outlying Islands", + dialCode: "+268", + }, + { + code: "VI", + name: "United States Virgin Islands", + dialCode: "+1340", + }, + { + code: "UY", + name: "Uruguay", + dialCode: "+598", + }, + { + code: "UZ", + name: "Uzbekistan", + dialCode: "+998", + }, + { + code: "VU", + name: "Vanuatu", + dialCode: "+678", + }, + { + code: "VA", + name: "Vatican City", + dialCode: "+39", + }, + { + code: "VE", + name: "Venezuela", + dialCode: "+58", + }, + { + code: "VN", + name: "Vietnam", + dialCode: "+84", + }, + { + code: "WF", + name: "Wallis and Futuna", + dialCode: "+681", + }, + { + code: "EH", + name: "Western Sahara", + dialCode: "+212", + }, + { + code: "YE", + name: "Yemen", + dialCode: "+967", + }, + { + code: "ZM", + name: "Zambia", + dialCode: "+260", + }, + { + code: "ZW", + name: "Zimbabwe", + dialCode: "+263", + }, +] as const + +export type CountryDialCode = (typeof COUNTRY_DIAL_CODES)[number] diff --git a/src/lib/mocks/tickets.ts b/src/lib/mocks/tickets.ts deleted file mode 100644 index 3a10363..0000000 --- a/src/lib/mocks/tickets.ts +++ /dev/null @@ -1,304 +0,0 @@ -import { addMinutes, subHours, subMinutes } from "date-fns" -import { z } from "zod" - -import { - commentVisibilitySchema, - ticketChannelSchema, - ticketPrioritySchema, - ticketSchema, - ticketStatusSchema, - ticketWithDetailsSchema, - ticketPlayContextSchema, - ticketQueueSummarySchema, -} from "@/lib/schemas/ticket" - -const tenantId = "tenant-atlas" - -type UserRecord = z.infer["requester"] - -const users: Record = { - rever: { - id: "user-rever", - name: "Rever", - email: "renan.pac@paulicon.com.br", - avatarUrl: "https://avatar.vercel.sh/rever", - teams: ["Chamados"], - }, - carla: { - id: "user-carla", - name: "Carla Menezes", - email: "carla.menezes@example.com", - avatarUrl: "https://avatar.vercel.sh/carla", - teams: ["Laboratório"], - }, - diego: { - id: "user-diego", - name: "Diego Ramos", - email: "diego.ramos@example.com", - avatarUrl: "https://avatar.vercel.sh/diego", - teams: ["Field Services"], - }, - eduarda: { - id: "user-eduarda", - name: "Eduarda Rocha", - email: "eduarda.rocha@example.com", - avatarUrl: "https://avatar.vercel.sh/eduarda", - teams: ["Clientes"], - }, -} - -const queues = [ - { id: "queue-chamados", name: "Chamados", pending: 18, waiting: 4, breached: 2 }, - { id: "queue-laboratorio", name: "Laboratório", pending: 9, waiting: 3, breached: 1 }, - { id: "queue-field", name: "Field Services", pending: 5, waiting: 2, breached: 0 }, -] - -const baseTickets = [ - { - id: "ticket-1001", - tenantId, - reference: 41001, - subject: "Erro 500 ao acessar portal do cliente", - summary: "Clientes relatam erro intermitente no portal web", - status: ticketStatusSchema.enum.AWAITING_ATTENDANCE, - priority: ticketPrioritySchema.enum.URGENT, - channel: ticketChannelSchema.enum.EMAIL, - queue: "Chamados", - requester: users.eduarda, - assignee: users.rever, - slaPolicy: { - id: "sla-critical", - name: "SLA Crítico", - targetMinutesToFirstResponse: 15, - targetMinutesToResolution: 240, - }, - dueAt: addMinutes(new Date(), 120), - firstResponseAt: subMinutes(new Date(), 20), - resolvedAt: null, - updatedAt: subMinutes(new Date(), 10), - createdAt: subHours(new Date(), 5), - tags: ["portal", "cliente"], - lastTimelineEntry: "Prioridade atualizada para URGENT por Rever", - metrics: { - timeWaitingMinutes: 12, - timeOpenedMinutes: 300, - }, - }, - { - id: "ticket-1002", - tenantId, - reference: 41002, - subject: "Integração ERP parada", - summary: "Webhook do ERP retornando timeout", - status: ticketStatusSchema.enum.PAUSED, - priority: ticketPrioritySchema.enum.HIGH, - channel: ticketChannelSchema.enum.WHATSAPP, - queue: "Laboratório", - requester: users.eduarda, - assignee: users.carla, - slaPolicy: { - id: "sla-priority", - name: "SLA Prioritário", - targetMinutesToFirstResponse: 30, - targetMinutesToResolution: 360, - }, - dueAt: addMinutes(new Date(), 240), - firstResponseAt: subMinutes(new Date(), 60), - resolvedAt: null, - updatedAt: subMinutes(new Date(), 30), - createdAt: subHours(new Date(), 8), - tags: ["Integração", "erp"], - lastTimelineEntry: "Aguardando retorno do fornecedor externo", - metrics: { - timeWaitingMinutes: 90, - timeOpenedMinutes: 420, - }, - }, - { - id: "ticket-1003", - tenantId, - reference: 41003, - subject: "Solicitação de acesso VPN", - summary: "Novo colaborador precisa de acesso", - status: ticketStatusSchema.enum.PENDING, - priority: ticketPrioritySchema.enum.MEDIUM, - channel: ticketChannelSchema.enum.MANUAL, - queue: "Field Services", - requester: users.eduarda, - assignee: null, - slaPolicy: null, - dueAt: null, - firstResponseAt: null, - resolvedAt: null, - updatedAt: subHours(new Date(), 1), - createdAt: subHours(new Date(), 1), - tags: ["vpn"], - metrics: null, - }, - { - id: "ticket-1004", - tenantId, - reference: 41004, - subject: "Bug no app mobile - upload de foto", - summary: "Upload trava com arquivos acima de 5MB", - status: ticketStatusSchema.enum.PAUSED, - priority: ticketPrioritySchema.enum.HIGH, - channel: ticketChannelSchema.enum.CHAT, - queue: "Laboratório", - requester: users.eduarda, - assignee: users.carla, - slaPolicy: { - id: "sla-standard", - name: "SLA Padrão", - targetMinutesToFirstResponse: 60, - targetMinutesToResolution: 720, - }, - dueAt: addMinutes(new Date(), 360), - firstResponseAt: subMinutes(new Date(), 50), - resolvedAt: null, - updatedAt: subMinutes(new Date(), 90), - createdAt: subHours(new Date(), 12), - tags: ["mobile", "upload"], - lastTimelineEntry: "Ticket pausado aguardando logs do time Mobile", - metrics: { - timeWaitingMinutes: 180, - timeOpenedMinutes: 720, - }, - }, - { - id: "ticket-1005", - tenantId, - reference: 41005, - subject: "Reclamação de cobranca duplicada", - summary: "Cliente recebeu boleto duplicado", - status: ticketStatusSchema.enum.RESOLVED, - priority: ticketPrioritySchema.enum.MEDIUM, - channel: ticketChannelSchema.enum.EMAIL, - queue: "Chamados", - requester: users.eduarda, - assignee: users.rever, - slaPolicy: { - id: "sla-standard", - name: "SLA Padrão", - targetMinutesToFirstResponse: 60, - targetMinutesToResolution: 720, - }, - dueAt: null, - firstResponseAt: subMinutes(new Date(), 80), - resolvedAt: subMinutes(new Date(), 10), - updatedAt: subMinutes(new Date(), 5), - createdAt: subHours(new Date(), 20), - tags: ["financeiro"], - lastTimelineEntry: "Ticket resolvido, aguardando confirmação do cliente", - metrics: { - timeWaitingMinutes: 30, - timeOpenedMinutes: 1100, - }, - }, -] - -export const tickets = baseTickets as Array> - -const commentsByTicket: Record["comments"]> = { - "ticket-1001": [ - { - id: "comment-1", - author: users.rever, - visibility: commentVisibilitySchema.enum.INTERNAL, - body: "Logs coletados e enviados para o time de infraestrutura.", - attachments: [], - createdAt: subMinutes(new Date(), 40), - updatedAt: subMinutes(new Date(), 40), - }, - { - id: "comment-2", - author: users.rever, - visibility: commentVisibilitySchema.enum.PUBLIC, - body: "Estamos investigando o incidente, retorno em 30 minutos.", - attachments: [], - createdAt: subMinutes(new Date(), 25), - updatedAt: subMinutes(new Date(), 25), - }, - ], - "ticket-1002": [ - { - id: "comment-3", - author: users.carla, - visibility: commentVisibilitySchema.enum.INTERNAL, - body: "Contato realizado com fornecedor, aguardando confirmação.", - attachments: [], - createdAt: subMinutes(new Date(), 70), - updatedAt: subMinutes(new Date(), 70), - }, - ], -} - -const timelineByTicket: Record["timeline"]> = { - "ticket-1001": [ - { - id: "timeline-1", - type: "STATUS_CHANGED", - payload: { from: "PENDING", to: "AWAITING_ATTENDANCE" }, - createdAt: subHours(new Date(), 5), - }, - { - id: "timeline-2", - type: "ASSIGNEE_CHANGED", - payload: { assignee: users.rever.name }, - createdAt: subHours(new Date(), 4), - }, - { - id: "timeline-3", - type: "COMMENT_ADDED", - payload: { author: users.rever.name }, - createdAt: subHours(new Date(), 1), - }, - ], - "ticket-1002": [ - { - id: "timeline-4", - type: "STATUS_CHANGED", - payload: { from: "AWAITING_ATTENDANCE", to: "PAUSED" }, - createdAt: subHours(new Date(), 3), - }, - ], -} - -export const ticketDetails = tickets.map((ticket) => ({ - ...ticket, - description: - "Incidente reportado automaticamente pelo monitoramento. Logs indicam aumento de latência em chamadas ao servico de autenticação.", - customFields: { - ambiente: { label: "Ambiente", type: "select", value: "producao", displayValue: "Produção" }, - categoria: { label: "Categoria", type: "text", value: "Incidente" }, - impacto: { label: "Impacto", type: "select", value: "alto", displayValue: "Alto" }, - }, - timeline: - timelineByTicket[ticket.id] ?? - ([ - { - id: `timeline-${ticket.id}`, - type: "CREATED", - createdAt: ticket.createdAt, - payload: { requester: ticket.requester.name }, - }, - ] as z.infer["timeline"]), - comments: commentsByTicket[ticket.id] ?? [], -})) as Array> - -export function getTicketById(id: string) { - return ticketDetails.find((ticket) => ticket.id === id) -} - -export const queueSummaries = queues as Array> - -export const playContext = { - queue: queueSummaries[0], - nextTicket: - tickets.find( - (ticket) => - ticket.status !== ticketStatusSchema.enum.RESOLVED - ) ?? null, -} as z.infer - -