diff --git a/apps/api/src/activities/activities.contracts.ts b/apps/api/src/activities/activities.contracts.ts index ad984fded..8fe91d232 100644 --- a/apps/api/src/activities/activities.contracts.ts +++ b/apps/api/src/activities/activities.contracts.ts @@ -108,6 +108,7 @@ const activityContactRefOutput = z id: z.string(), firstName: z.string(), lastName: z.string().nullable(), + phone: z.string().nullable(), }) .nullable(); diff --git a/apps/api/src/activities/activities.service.ts b/apps/api/src/activities/activities.service.ts index 50439fcbc..46e5fc849 100644 --- a/apps/api/src/activities/activities.service.ts +++ b/apps/api/src/activities/activities.service.ts @@ -38,7 +38,9 @@ const ENTRY_SELECT = { createdAt: true, createdBy: { select: AUTHOR_SELECT }, company: { select: { id: true, name: true } }, - contact: { select: { id: true, firstName: true, lastName: true } }, + contact: { + select: { id: true, firstName: true, lastName: true, phone: true }, + }, deal: { select: { id: true, name: true } }, emailThread: { diff --git a/apps/app/app/(app)/[slug]/cockpit/commercial-cockpit.tsx b/apps/app/app/(app)/[slug]/cockpit/commercial-cockpit.tsx new file mode 100644 index 000000000..0dac76de8 --- /dev/null +++ b/apps/app/app/(app)/[slug]/cockpit/commercial-cockpit.tsx @@ -0,0 +1,378 @@ +"use client"; + +import Phone from "@carbon/icons-react/es/Phone"; +import Task from "@carbon/icons-react/es/Task"; +import type { DealStage } from "@crm/db/enums"; +import { Button } from "@crm/ui/components/button"; +import { + Card, + CardDescription, + CardHeader, + CardPanel, + CardTitle, +} from "@crm/ui/components/card"; +import { Icon } from "@crm/ui/components/icon"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@crm/ui/components/tabs"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import Link from "next/link"; +import { useMemo, useState } from "react"; +import { toast } from "sonner"; +import { useOpenRecord } from "@/components/crm/record-sheet/record-stack"; +import { LocalDay } from "@/components/local-date-time"; +import { dealStageLabel } from "@/lib/deal-stage"; +import { useCrmCache } from "@/lib/trpc/cache"; +import { useTRPC } from "@/lib/trpc/client"; +import type { RouterOutputs } from "@/lib/trpc/types"; +import { useWorkspaceUrl } from "@/lib/use-workspace-url"; + +type TaskEntry = RouterOutputs["activities"]["myTasks"][number]; +type DealRow = RouterOutputs["deals"]["list"]["rows"][number]; + +type Bucket = "overdue" | "today" | "upcoming" | "no-date"; + +const BUCKETS: Array<{ id: Bucket; label: string; description: string }> = [ + { + id: "overdue", + label: "Atrasados", + description: "Ações que já passaram do prazo", + }, + { id: "today", label: "Hoje", description: "Ações para executar agora" }, + { id: "upcoming", label: "Próximos", description: "Ações dos próximos dias" }, + { + id: "no-date", + label: "Sem data", + description: "Tarefas que precisam de planejamento", + }, +]; + +function dayKey(value: Date): string { + return `${value.getFullYear()}-${value.getMonth()}-${value.getDate()}`; +} + +function bucketFor(task: TaskEntry): Bucket { + if (!task.dueAt) return "no-date"; + const due = new Date(task.dueAt); + const today = new Date(); + if (due < new Date(today.getFullYear(), today.getMonth(), today.getDate())) { + return "overdue"; + } + if (dayKey(due) === dayKey(today)) return "today"; + return "upcoming"; +} + +function taskLabel(task: TaskEntry): string { + return task.subject ?? task.body ?? "Follow-up comercial"; +} + +function whatsappUrl(task: TaskEntry): string | null { + const phone = task.contact?.phone?.replace(/\D/g, ""); + if (!phone) return null; + const text = + task.body || + `Olá${task.contact?.firstName ? `, ${task.contact.firstName}` : ""}! Passando para retomar nossa conversa. Podemos avançar por aqui?`; + return `https://wa.me/${phone}?text=${encodeURIComponent(text)}`; +} + +function TaskRow({ task }: { task: TaskEntry }) { + const trpc = useTRPC(); + const cache = useCrmCache(); + const openRecord = useOpenRecord(); + const complete = useMutation( + trpc.activities.complete.mutationOptions({ + onSuccess: () => cache.activity(), + onError: (error) => toast.error(error.message), + }), + ); + const whatsapp = whatsappUrl(task); + const isCall = + (task.subject?.toLowerCase().includes("ligação") || + task.subject?.toLowerCase().includes("ligar")) ?? + false; + const isPreview = task.subject?.toLowerCase().includes("prévia") ?? false; + const cleanPhone = task.contact?.phone?.replace(/\D/g, ""); + const record = task.deal + ? { kind: "deal" as const, id: task.deal.id } + : task.contact + ? { kind: "contact" as const, id: task.contact.id } + : task.company + ? { kind: "company" as const, id: task.company.id } + : null; + + return ( +
Nenhuma oportunidade no Kanban.
++ Crie um Deal para acompanhar a oportunidade por estágio. +
+ ++ Nenhum follow-up nesta fila. +
+ ) : ( + rows.map((task) =>