arcadiasuite/client/src/pages/ProcessCompass.tsx

6121 lines
290 KiB
TypeScript

import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { useState, useEffect } from "react";
import BpmnDiagram from "@/components/BpmnDiagram";
import { useLocation } from "wouter";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Slider } from "@/components/ui/slider";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import { Checkbox } from "@/components/ui/checkbox";
import {
LayoutDashboard,
Users,
FolderKanban,
Target,
TrendingUp,
FileText,
Plus,
Trash2,
Settings,
Loader2,
Building2,
Phone,
Mail,
Globe,
MapPin,
ChevronRight,
Calendar,
Clock,
BarChart3,
Briefcase,
UserPlus,
Search,
Filter,
MoreVertical,
Edit,
Eye,
CheckCircle,
AlertCircle,
Compass,
Network,
ClipboardList,
Layers,
Menu,
X,
RefreshCw,
FileCheck,
HelpCircle,
Info,
ExternalLink,
Download,
Lightbulb,
Pencil,
LayoutGrid,
} from "lucide-react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
} from "@/components/ui/dropdown-menu";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { toast } from "sonner";
import { ProcessDiagram } from "@/components/ProcessDiagram";
import { RichTextEditor } from "@/components/RichTextEditor";
import { Upload, UserX, Save, CirclePlay } from "lucide-react";
interface CompassStats {
clients: number;
projects: number;
leads: number;
opportunities: number;
activeProjects: number;
openOpportunities: number;
}
interface PcClient {
id: number;
tenantId: number;
userId: string;
name: string;
email: string | null;
phone: string | null;
company: string | null;
industry: string | null;
website: string | null;
address: string | null;
notes: string | null;
logoUrl: string | null;
status: string;
createdAt: string;
}
interface PcProject {
id: number;
tenantId: number;
clientId: number;
name: string;
description: string | null;
projectType: string;
status: string;
startDate: string | null;
dueDate: string | null;
priority: number;
createdAt: string;
}
interface PcCrmLead {
id: number;
name: string;
email: string | null;
phone: string | null;
company: string | null;
source: string | null;
status: string;
createdAt: string;
}
interface PcCrmOpportunity {
id: number;
name: string;
value: number;
probability: number;
status: string;
expectedCloseDate: string | null;
createdAt: string;
}
interface Tenant {
id: number;
name: string;
slug: string | null;
plan: string;
status: string;
}
interface PcCanvasBlock {
id: number;
projectId: number;
blockType: string;
level: string;
title: string | null;
content: string | null;
notes: string | null;
score: number;
synthesis: string | null;
}
interface PcCanvasQuestion {
id: number;
blockId: number;
question: string;
answer: string | null;
score: number | null;
orderIndex: number;
}
interface PcCanvasExpectedOutput {
id: number;
blockId: number;
description: string;
completed: boolean;
orderIndex: number;
}
interface PcCanvasPdcaLink {
id: number;
blockId: number;
pdcaActionId: number;
observation: string | null;
}
interface PcCanvasSwotLink {
id: number;
blockId: number;
swotItemId: number;
}
interface PcProcess {
id: number;
projectId: number;
name: string;
description: string | null;
category: string | null;
owner: string | null;
status: string;
priority: number;
diagramNodes?: any[];
diagramEdges?: any[];
diagramViewport?: { x: number; y: number; zoom: number };
}
interface PcProcessStep {
id: number;
processId: number;
name: string;
description: string | null;
responsible: string | null;
inputs: string | null;
outputs: string | null;
systems: string | null;
duration: string | null;
painPoints: string | null;
suggestions: string | null;
order: number;
}
interface PcSwotAnalysis {
id: number;
projectId: number;
name: string;
description: string | null;
sector: string | null;
status: string;
}
interface PcSwotItem {
id: number;
swotAnalysisId: number;
type: string;
title: string | null;
description: string;
impact: string;
impactScore: number;
priorityLevel: string;
priority: number;
actionPlan: string | null;
result: string | null;
pdcaStatus: string;
responsible: string | null;
dueDate: string | null;
status: string;
}
interface PcPdcaCycle {
id: number;
tenantId: number;
projectId: number | null;
title: string;
description: string | null;
status: string;
priority: string;
dueDate: string | null;
createdAt: string;
}
interface PdcaOverviewItem {
id: number;
source: 'canvas' | 'processes' | 'swot' | 'requirements';
title: string;
description: string | null;
pdcaStatus: string;
pdcaActionPlan: string | null;
pdcaResult: string | null;
projectId: number;
}
interface PdcaOverviewStats {
total: number;
plan: number;
do: number;
check: number;
act: number;
done: number;
}
interface PdcaOverview {
items: PdcaOverviewItem[];
stats: {
canvas: PdcaOverviewStats;
processes: PdcaOverviewStats;
swot: PdcaOverviewStats;
requirements: PdcaOverviewStats;
};
}
interface PcPdcaAction {
id: number;
cycleId: number;
phase: string;
title: string;
description: string | null;
responsible: string | null;
status: string;
dueDate: string | null;
}
interface PcRequirement {
id: number;
tenantId: number;
projectId: number | null;
code: string | null;
title: string;
description: string | null;
type: string;
priority: string;
status: string;
source: string | null;
category: string | null;
acceptanceCriteria: string | null;
createdAt: string;
}
interface PcTeamMember {
id: number;
projectId: number;
name: string;
email: string | null;
role: string;
createdAt: string;
}
interface PcProjectFile {
id: number;
projectId: number;
name: string;
originalName: string;
mimeType: string;
size: number;
url: string;
createdAt: string;
}
interface PcProjectTask {
id: number;
projectId: number;
title: string;
description: string | null;
status: string;
priority: string;
assignedTo: string | null;
dueDate: string | null;
createdAt: string;
}
interface PcProjectHistory {
id: number;
projectId: number;
content: string;
updatedAt: string;
}
const PDCA_PHASES = [
{ id: "plan", label: "Plan", color: "bg-blue-100 text-blue-800" },
{ id: "do", label: "Do", color: "bg-green-100 text-green-800" },
{ id: "check", label: "Check", color: "bg-yellow-100 text-yellow-800" },
{ id: "act", label: "Act", color: "bg-purple-100 text-purple-800" },
];
const REQUIREMENT_TYPES = [
{ id: "functional", label: "Funcional" },
{ id: "non_functional", label: "Não Funcional" },
{ id: "business", label: "Negócio" },
{ id: "technical", label: "Técnico" },
];
const REQUIREMENT_STATUS = [
{ id: "draft", label: "Rascunho" },
{ id: "approved", label: "Aprovado" },
{ id: "implemented", label: "Implementado" },
{ id: "verified", label: "Verificado" },
{ id: "rejected", label: "Rejeitado" },
];
const CANVAS_BLOCK_TYPES = [
{ id: "value_propositions", label: "Proposta de Valor", color: "bg-green-100 border-green-300", description: "Sentido / Propósito" },
{ id: "customer_segments", label: "Segmentos de Clientes", color: "bg-orange-100 border-orange-300", description: "Quem recebe nossa transformação" },
{ id: "channels", label: "Canais", color: "bg-yellow-100 border-yellow-300", description: "Caminhos / Pontes" },
{ id: "customer_relationships", label: "Relacionamento", color: "bg-yellow-100 border-yellow-300", description: "Forma de Amar/Servir" },
{ id: "revenue_streams", label: "Fontes de Receita", color: "bg-emerald-100 border-emerald-300", description: "Energia que sustenta o propósito" },
{ id: "key_resources", label: "Recursos-Chave", color: "bg-blue-100 border-blue-300", description: "Capacidades / Disposições" },
{ id: "key_activities", label: "Atividades-Chave", color: "bg-blue-100 border-blue-300", description: "Ações / Ritmos" },
{ id: "key_partners", label: "Parcerias-Chave", color: "bg-purple-100 border-purple-300", description: "Alianças e interdependência" },
{ id: "cost_structure", label: "Estrutura de Custos", color: "bg-red-100 border-red-300", description: "Sacrifícios / Investimentos" },
];
const CANVAS_LEVELS = [
{ id: "atual", label: "Atual", description: "Estado atual do negócio - como está hoje" },
{ id: "sistemico", label: "Sistêmico", description: "Visão sistêmica e estratégica" },
{ id: "intencao", label: "Intenção", description: "O que pretendemos fazer" },
{ id: "evidencias", label: "Evidências", description: "O que temos de concreto" },
{ id: "transformacao", label: "Transformação", description: "O que queremos mudar" },
];
const PROCESS_CATEGORIES = [
{ id: "operacional", label: "Operacional" },
{ id: "comercial", label: "Comercial" },
{ id: "administrativo", label: "Administrativo" },
{ id: "financeiro", label: "Financeiro" },
];
const SWOT_SECTORS = [
{ id: "general", label: "Geral" },
{ id: "comercial", label: "Comercial" },
{ id: "operacional", label: "Operacional" },
{ id: "financeiro", label: "Financeiro" },
{ id: "rh", label: "Recursos Humanos" },
];
const INDUSTRY_TEMPLATES: Record<string, { strengths: string[]; weaknesses: string[]; opportunities: string[]; threats: string[] }> = {
tecnologia: {
strengths: ["Inovação e desenvolvimento ágil", "Equipe técnica qualificada", "Escalabilidade de soluções", "Cultura de aprendizado contínuo"],
weaknesses: ["Alta rotatividade de talentos", "Dependência de tecnologias específicas", "Custos elevados de P&D", "Documentação técnica deficiente"],
opportunities: ["Transformação digital do mercado", "Novos nichos e verticais", "Parcerias estratégicas", "Expansão internacional"],
threats: ["Concorrência global intensa", "Mudanças tecnológicas rápidas", "Regulamentações de dados (LGPD)", "Ataques cibernéticos"],
},
varejo: {
strengths: ["Localização privilegiada", "Conhecimento do cliente local", "Variedade de produtos", "Atendimento personalizado"],
weaknesses: ["Margens reduzidas", "Gestão de estoque complexa", "Sazonalidade de vendas", "Alto custo de aluguel"],
opportunities: ["E-commerce e omnichannel", "Programas de fidelidade", "Novos canais de venda", "Parcerias com influenciadores"],
threats: ["Concorrência online", "Mudanças de hábitos de consumo", "Flutuações econômicas", "Aumento de custos logísticos"],
},
servicos: {
strengths: ["Expertise especializada", "Relacionamento com clientes", "Flexibilidade de entrega", "Baixo investimento inicial"],
weaknesses: ["Dependência de pessoas-chave", "Dificuldade de escalar", "Precificação subjetiva", "Gestão de expectativas"],
opportunities: ["Digitalização de serviços", "Novos modelos de assinatura", "Expansão de portfólio", "Certificações e especializações"],
threats: ["Commoditização de serviços", "Novos entrantes", "Automação de processos", "Pressão por redução de preços"],
},
manufatura: {
strengths: ["Capacidade produtiva instalada", "Controle de qualidade", "Economia de escala", "Expertise técnica"],
weaknesses: ["Alto custo fixo", "Obsolescência de equipamentos", "Dependência de fornecedores", "Lead time extenso"],
opportunities: ["Indústria 4.0 e IoT", "Novos mercados de exportação", "Sustentabilidade e ESG", "Customização em massa"],
threats: ["Importação de produtos mais baratos", "Volatilidade de matérias-primas", "Regulamentações ambientais", "Escassez de mão de obra qualificada"],
},
saude: {
strengths: ["Profissionais qualificados", "Infraestrutura especializada", "Credibilidade institucional", "Demanda constante"],
weaknesses: ["Custos operacionais elevados", "Burocracia regulatória", "Gestão de agenda complexa", "Alta dependência de convênios"],
opportunities: ["Telemedicina e saúde digital", "Turismo médico", "Novos tratamentos e tecnologias", "Programas preventivos"],
threats: ["Mudanças em planos de saúde", "Judicialização", "Novas regulamentações ANVISA", "Concorrência de grandes redes"],
},
};
const MODULE_HELP: Record<string, { title: string; description: string; tips: string[] }> = {
canvas: {
title: "Canvas BMC Expandido",
description: "Diagnóstico estratégico em múltiplos níveis evolutivos para análise completa do modelo de negócio.",
tips: [
"Use os níveis Atual e Sistêmico para diagnóstico estratégico",
"Cada bloco tem perguntas diagnósticas para avaliação de maturidade",
"A maturidade geral mostra o progresso do diagnóstico",
],
},
swot: {
title: "Análise SWOT",
description: "Metodologia estratégica para analisar Forças, Fraquezas, Oportunidades e Ameaças do negócio.",
tips: [
"Use templates por indústria para acelerar sua análise",
"Cruze forças com oportunidades para criar estratégias ofensivas (SO)",
"Identifique ameaças que podem explorar suas fraquezas (WT)",
],
},
pdca: {
title: "Ciclo PDCA",
description: "Metodologia de melhoria contínua em 4 fases: Planejar (Plan), Executar (Do), Verificar (Check), Agir (Act).",
tips: [
"Defina métricas claras na fase Plan",
"Documente aprendizados na fase Check",
"Use Act para padronizar melhorias que funcionaram",
],
},
processes: {
title: "Mapeamento de Processos",
description: "Documentação detalhada dos processos de negócio, incluindo entradas, saídas e pontos de dor.",
tips: [
"Identifique os processos críticos primeiro",
"Documente os pain points para priorizar melhorias",
"Defina responsáveis claros para cada etapa",
],
},
};
const statusColors: Record<string, string> = {
backlog: "bg-gray-100 text-gray-800",
diagnostico: "bg-blue-100 text-blue-800",
andamento: "bg-yellow-100 text-yellow-800",
revisao: "bg-purple-100 text-purple-800",
concluido: "bg-green-100 text-green-800",
active: "bg-green-100 text-green-800",
inactive: "bg-gray-100 text-gray-800",
prospect: "bg-blue-100 text-blue-800",
new: "bg-blue-100 text-blue-800",
contacted: "bg-yellow-100 text-yellow-800",
qualified: "bg-purple-100 text-purple-800",
converted: "bg-green-100 text-green-800",
lost: "bg-red-100 text-red-800",
open: "bg-blue-100 text-blue-800",
won: "bg-green-100 text-green-800",
};
const statusLabels: Record<string, string> = {
backlog: "Backlog",
diagnostico: "Diagnóstico",
andamento: "Em Andamento",
revisao: "Revisão",
concluido: "Concluído",
active: "Ativo",
inactive: "Inativo",
prospect: "Prospecto",
new: "Novo",
contacted: "Contatado",
qualified: "Qualificado",
converted: "Convertido",
lost: "Perdido",
open: "Aberta",
won: "Ganha",
};
export default function ProcessCompass() {
const [, navigate] = useLocation();
const [activeTab, setActiveTab] = useState("dashboard");
const [projectDetailTab, setProjectDetailTab] = useState("overview");
const [activeValuationSubTab, setActiveValuationSubTab] = useState("overview");
const [showNewClientDialog, setShowNewClientDialog] = useState(false);
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false);
const [showNewLeadDialog, setShowNewLeadDialog] = useState(false);
const [showNewTenantDialog, setShowNewTenantDialog] = useState(false);
const [showNewProcessDialog, setShowNewProcessDialog] = useState(false);
const [showNewSwotDialog, setShowNewSwotDialog] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [selectedProjectId, setSelectedProjectId] = useState<number | null>(null);
const [selectedCanvasLevel, setSelectedCanvasLevel] = useState("atual");
const [selectedProcessId, setSelectedProcessId] = useState<number | null>(null);
const [processView, setProcessView] = useState<"steps" | "diagram">("steps");
const [selectedSwotId, setSelectedSwotId] = useState<number | null>(null);
const [editingBlock, setEditingBlock] = useState<PcCanvasBlock | null>(null);
const [canvasDialogTab, setCanvasDialogTab] = useState("content");
const [newQuestionText, setNewQuestionText] = useState("");
const [newOutputText, setNewOutputText] = useState("");
const [newProcessCategory, setNewProcessCategory] = useState("");
const [newSwotSector, setNewSwotSector] = useState("");
const [sidebarOpen, setSidebarOpen] = useState(false);
const [selectedIndustryTemplate, setSelectedIndustryTemplate] = useState<string | null>(null);
const [showModuleHelp, setShowModuleHelp] = useState<string | null>(null);
const [showNewPdcaDialog, setShowNewPdcaDialog] = useState(false);
const [showNewRequirementDialog, setShowNewRequirementDialog] = useState(false);
const [newReqTitle, setNewReqTitle] = useState("");
const [newReqDescription, setNewReqDescription] = useState("");
const [newReqType, setNewReqType] = useState("functional");
const [newReqPriority, setNewReqPriority] = useState("medium");
const [showValuationProjectDialog, setShowValuationProjectDialog] = useState(false);
const [showEditProjectDialog, setShowEditProjectDialog] = useState(false);
const [editingProject, setEditingProject] = useState<PcProject | null>(null);
const [showDeleteProjectConfirm, setShowDeleteProjectConfirm] = useState(false);
const [projectToDelete, setProjectToDelete] = useState<PcProject | null>(null);
const [selectedPdcaId, setSelectedPdcaId] = useState<number | null>(null);
const [editingSwotItem, setEditingSwotItem] = useState<PcSwotItem | null>(null);
const [swotItemEditTab, setSwotItemEditTab] = useState("details");
const [swotPdcaStatus, setSwotPdcaStatus] = useState("plan");
const [pdcaPhaseFilter, setPdcaPhaseFilter] = useState<string>("all");
const [pdcaSourceFilter, setPdcaSourceFilter] = useState<string>("all");
const [showReportConfigModal, setShowReportConfigModal] = useState(false);
const [showProcessDiagramDialog, setShowProcessDiagramDialog] = useState(false);
const [diagramProcessId, setDiagramProcessId] = useState<number | null>(null);
const [showTeamMemberDialog, setShowTeamMemberDialog] = useState(false);
const [showNewTaskDialog, setShowNewTaskDialog] = useState(false);
const [showFileUploadDialog, setShowFileUploadDialog] = useState(false);
const [projectHistoryContent, setProjectHistoryContent] = useState("");
const [historyIsSaving, setHistoryIsSaving] = useState(false);
const [newMemberRole, setNewMemberRole] = useState("");
const [newTaskPriority, setNewTaskPriority] = useState("medium");
const [showReportEditorDialog, setShowReportEditorDialog] = useState(false);
const [editingReport, setEditingReport] = useState<any>(null);
const [reportContent, setReportContent] = useState("");
const [reportName, setReportName] = useState("");
const [reportIsSaving, setReportIsSaving] = useState(false);
const [reportConfigTemplate, setReportConfigTemplate] = useState("full_diagnostic");
const [reportConfigSections, setReportConfigSections] = useState<string[]>([
"project_info", "history", "canvas_atual", "swot", "processes", "pdca", "tasks", "requirements"
]);
const queryClient = useQueryClient();
const REPORT_SECTIONS = [
{ id: "project_info", label: "Informações do Projeto", description: "Dados básicos do projeto e cliente", icon: "building" },
{ id: "history", label: "História do Projeto", description: "Contexto, evolução e documentação do projeto", icon: "book" },
{ id: "canvas_atual", label: "Canvas BMC - Atual", description: "Business Model Canvas nível Atual (Intenção)", icon: "layout" },
{ id: "canvas_sistemico", label: "Canvas BMC - Sistêmico", description: "Business Model Canvas nível Sistêmico", icon: "layout" },
{ id: "swot", label: "Análise SWOT", description: "Forças, Fraquezas, Oportunidades e Ameaças", icon: "grid" },
{ id: "processes", label: "Processos", description: "Mapeamento de processos de negócio", icon: "workflow" },
{ id: "pdca", label: "Plano PDCA Consolidado", description: "Ciclos de melhoria contínua", icon: "refresh" },
{ id: "deliverables", label: "Entregas", description: "Entregáveis e marcos do projeto", icon: "package" },
{ id: "tasks", label: "Tarefas", description: "Atividades e responsáveis", icon: "check" },
{ id: "requirements", label: "Requisitos ERP", description: "Requisitos de aderência ao ERP", icon: "database" },
];
const toggleReportSection = (sectionId: string) => {
setReportConfigSections(prev =>
prev.includes(sectionId)
? prev.filter(s => s !== sectionId)
: [...prev, sectionId]
);
};
const { data: stats, isLoading: statsLoading } = useQuery<CompassStats>({
queryKey: ["/api/compass/stats"],
});
const { data: tenants = [], isLoading: tenantsLoading } = useQuery<Tenant[]>({
queryKey: ["/api/compass/tenants"],
});
const { data: clients = [], isLoading: clientsLoading } = useQuery<PcClient[]>({
queryKey: ["/api/compass/clients"],
});
const { data: projects = [], isLoading: projectsLoading } = useQuery<PcProject[]>({
queryKey: ["/api/compass/projects"],
});
const { data: canvasBlocks = [] } = useQuery<PcCanvasBlock[]>({
queryKey: ["/api/compass/projects", selectedProjectId, "canvas"],
queryFn: async () => {
if (!selectedProjectId) return [];
const res = await fetch(`/api/compass/projects/${selectedProjectId}/canvas`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedProjectId,
});
const { data: canvasQuestions = [] } = useQuery<PcCanvasQuestion[]>({
queryKey: ["/api/compass/canvas", editingBlock?.id, "questions"],
queryFn: async () => {
if (!editingBlock?.id) return [];
const res = await fetch(`/api/compass/canvas/${editingBlock.id}/questions`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!editingBlock?.id,
});
const { data: canvasOutputs = [] } = useQuery<PcCanvasExpectedOutput[]>({
queryKey: ["/api/compass/canvas", editingBlock?.id, "outputs"],
queryFn: async () => {
if (!editingBlock?.id) return [];
const res = await fetch(`/api/compass/canvas/${editingBlock.id}/outputs`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!editingBlock?.id,
});
const { data: canvasPdcaLinks = [] } = useQuery<PcCanvasPdcaLink[]>({
queryKey: ["/api/compass/canvas", editingBlock?.id, "pdca"],
queryFn: async () => {
if (!editingBlock?.id) return [];
const res = await fetch(`/api/compass/canvas/${editingBlock.id}/pdca`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!editingBlock?.id,
});
const { data: canvasSwotLinks = [] } = useQuery<PcCanvasSwotLink[]>({
queryKey: ["/api/compass/canvas", editingBlock?.id, "swot"],
queryFn: async () => {
if (!editingBlock?.id) return [];
const res = await fetch(`/api/compass/canvas/${editingBlock.id}/swot`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!editingBlock?.id,
});
const { data: processes = [] } = useQuery<PcProcess[]>({
queryKey: ["/api/compass/projects", selectedProjectId, "processes"],
queryFn: async () => {
if (!selectedProjectId) return [];
const res = await fetch(`/api/compass/projects/${selectedProjectId}/processes`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedProjectId,
});
const { data: processSteps = [] } = useQuery<PcProcessStep[]>({
queryKey: ["/api/compass/processes", selectedProcessId, "steps"],
queryFn: async () => {
if (!selectedProcessId) return [];
const res = await fetch(`/api/compass/processes/${selectedProcessId}/steps`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedProcessId,
});
const { data: swotAnalyses = [] } = useQuery<PcSwotAnalysis[]>({
queryKey: ["/api/compass/projects", selectedProjectId, "swot"],
queryFn: async () => {
if (!selectedProjectId) return [];
const res = await fetch(`/api/compass/projects/${selectedProjectId}/swot`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedProjectId,
});
const { data: swotItems = [] } = useQuery<PcSwotItem[]>({
queryKey: ["/api/compass/swot", selectedSwotId, "items"],
queryFn: async () => {
if (!selectedSwotId) return [];
const res = await fetch(`/api/compass/swot/${selectedSwotId}/items`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedSwotId,
});
const { data: pdcaCycles = [] } = useQuery<PcPdcaCycle[]>({
queryKey: ["/api/compass/pdca", selectedProjectId],
queryFn: async () => {
const url = selectedProjectId
? `/api/compass/pdca?projectId=${selectedProjectId}`
: `/api/compass/pdca`;
const res = await fetch(url, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
});
const { data: pdcaOverview } = useQuery<PdcaOverview>({
queryKey: ["/api/compass/pdca/overview", selectedProjectId],
queryFn: async () => {
if (!selectedProjectId) return { items: [], stats: { canvas: { total: 0, plan: 0, do: 0, check: 0, act: 0, done: 0 }, processes: { total: 0, plan: 0, do: 0, check: 0, act: 0, done: 0 }, swot: { total: 0, plan: 0, do: 0, check: 0, act: 0, done: 0 }, requirements: { total: 0, plan: 0, do: 0, check: 0, act: 0, done: 0 } } };
const res = await fetch(`/api/compass/pdca/overview/${selectedProjectId}`, { credentials: "include" });
if (!res.ok) return { items: [], stats: { canvas: { total: 0, plan: 0, do: 0, check: 0, act: 0, done: 0 }, processes: { total: 0, plan: 0, do: 0, check: 0, act: 0, done: 0 }, swot: { total: 0, plan: 0, do: 0, check: 0, act: 0, done: 0 }, requirements: { total: 0, plan: 0, do: 0, check: 0, act: 0, done: 0 } } };
return res.json();
},
enabled: !!selectedProjectId,
});
// Valuation-specific queries for the integrated tab
const selectedProject = projects.find((p) => p.id === selectedProjectId);
const { data: valuationSectorAnalysis } = useQuery<any>({
queryKey: ["/api/valuation/projects", selectedProjectId, "sector-analysis"],
queryFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProjectId}/sector-analysis`);
return res.json();
},
enabled: !!selectedProjectId && selectedProject?.projectType === "valuation",
});
const { data: valuationChecklist = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/projects", selectedProjectId, "checklist"],
queryFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProjectId}/checklist`);
return res.json();
},
enabled: !!selectedProjectId && selectedProject?.projectType === "valuation",
});
const { data: valuationCanvasBlocks = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/projects", selectedProjectId, "valuation-canvas"],
queryFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProjectId}/canvas`);
return res.json();
},
enabled: !!selectedProjectId && selectedProject?.projectType === "valuation",
});
const { data: generatedReports = [] } = useQuery<any[]>({
queryKey: ["/api/compass/generated-reports", selectedProjectId],
queryFn: async () => {
const res = await fetch(`/api/compass/generated-reports?projectId=${selectedProjectId}`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedProjectId,
});
const { data: projectTeamMembers = [] } = useQuery<PcTeamMember[]>({
queryKey: ["/api/compass/projects", selectedProjectId, "team"],
queryFn: async () => {
if (!selectedProjectId) return [];
const res = await fetch(`/api/compass/projects/${selectedProjectId}/team`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedProjectId,
});
const { data: projectTasks = [] } = useQuery<PcProjectTask[]>({
queryKey: ["/api/compass/projects", selectedProjectId, "tasks"],
queryFn: async () => {
if (!selectedProjectId) return [];
const res = await fetch(`/api/compass/projects/${selectedProjectId}/tasks`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedProjectId,
});
const { data: projectFiles = [] } = useQuery<PcProjectFile[]>({
queryKey: ["/api/compass/projects", selectedProjectId, "files"],
queryFn: async () => {
if (!selectedProjectId) return [];
const res = await fetch(`/api/compass/projects/${selectedProjectId}/files`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedProjectId,
});
const { data: projectHistory } = useQuery<PcProjectHistory>({
queryKey: ["/api/compass/projects", selectedProjectId, "history"],
queryFn: async () => {
if (!selectedProjectId) return null;
const res = await fetch(`/api/compass/projects/${selectedProjectId}/history`, { credentials: "include" });
if (!res.ok) return null;
return res.json();
},
enabled: !!selectedProjectId,
});
useEffect(() => {
if (projectHistory?.content) {
setProjectHistoryContent(projectHistory.content);
} else {
setProjectHistoryContent("");
}
}, [projectHistory]);
const generateReportMutation = useMutation({
mutationFn: async ({ projectId, name, templateType, format, sections }: { projectId: number; name: string; templateType: string; format: string; sections?: string[] }) => {
const res = await fetch("/api/compass/generated-reports", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ projectId, name, reportType: templateType, format, status: "pending", sections }),
});
if (!res.ok) throw new Error("Falha ao criar relatório");
const report = await res.json();
const generateRes = await fetch(`/api/compass/generated-reports/${report.id}/generate`, {
method: "POST",
credentials: "include",
});
return generateRes.json();
},
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/generated-reports"] });
toast.success("Relatório gerado com sucesso!");
if (data && data.content) {
setEditingReport(data);
setReportName(data.name || "");
setReportContent(data.content || "");
setShowReportEditorDialog(true);
}
},
onError: () => {
toast.error("Erro ao gerar relatório");
},
});
const handleQuickReport = (templateType: string) => {
if (!selectedProjectId) return;
const templateNames: Record<string, string> = {
executive_summary: "Sumário Executivo",
full_diagnostic: "Diagnóstico Completo",
swot_report: "Análise SWOT",
};
const project = projects.find(p => p.id === selectedProjectId);
const reportName = `${templateNames[templateType] || templateType} - ${project?.name || "Projeto"}`;
generateReportMutation.mutate({ projectId: selectedProjectId, name: reportName, templateType, format: "pdf" });
};
const openReportEditor = async (report: any) => {
setEditingReport(report);
setReportName(report.name || "");
setReportContent(report.content || "");
setShowReportEditorDialog(true);
};
const saveReportContent = async () => {
if (!editingReport) return;
setReportIsSaving(true);
try {
const res = await fetch(`/api/compass/generated-reports/${editingReport.id}/content`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ content: reportContent, name: reportName }),
});
if (!res.ok) throw new Error("Falha ao salvar");
queryClient.invalidateQueries({ queryKey: ["/api/compass/generated-reports"] });
toast.success("Relatório salvo com sucesso!");
} catch (error) {
toast.error("Erro ao salvar relatório");
} finally {
setReportIsSaving(false);
}
};
const exportReportAsPDF = async () => {
if (!reportContent) return;
const printWindow = window.open("", "_blank");
if (printWindow) {
printWindow.document.write(`
<!DOCTYPE html>
<html>
<head>
<title>${reportName}</title>
<style>
@media print {
body { margin: 0; padding: 20px; }
}
body { font-family: 'Segoe UI', Arial, sans-serif; }
</style>
</head>
<body>${reportContent}</body>
</html>
`);
printWindow.document.close();
setTimeout(() => printWindow.print(), 500);
}
};
const initializeValuationChecklistMutation = useMutation({
mutationFn: async () => {
const segment = selectedProject?.name?.includes("Indústria") ? "industry" :
selectedProject?.name?.includes("Serviços") ? "services" :
selectedProject?.name?.includes("Varejo") ? "retail" :
selectedProject?.name?.includes("Tecnologia") ? "technology" : "general";
const res = await fetch(`/api/valuation/projects/${selectedProjectId}/checklist/initialize`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ segment }),
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/valuation/projects", selectedProjectId, "checklist"] });
},
});
const VALUATION_CANVAS_BLOCKS = [
{ id: "customer_segments", label: "Segmentos de Clientes", description: "Para quem criamos valor?" },
{ id: "value_proposition", label: "Proposta de Valor", description: "Que valor entregamos?" },
{ id: "channels", label: "Canais", description: "Como alcançamos nossos clientes?" },
{ id: "customer_relationships", label: "Relacionamento", description: "Como nos relacionamos?" },
{ id: "revenue_streams", label: "Fontes de Receita", description: "Como geramos receita?" },
{ id: "key_resources", label: "Recursos Principais", description: "Quais recursos são essenciais?" },
{ id: "key_activities", label: "Atividades Principais", description: "O que fazemos de mais importante?" },
{ id: "key_partnerships", label: "Parcerias Principais", description: "Quem são nossos parceiros?" },
{ id: "cost_structure", label: "Estrutura de Custos", description: "Quais são nossos custos?" },
];
const getValuationBlockData = (blockType: string) => {
if (!Array.isArray(valuationCanvasBlocks)) return undefined;
return valuationCanvasBlocks.find((b: any) => b.blockType === blockType);
};
const getValuationScoreLevel = (score: number) => {
if (score >= 80) return { label: "Excelente", color: "text-emerald-600" };
if (score >= 60) return { label: "Bom", color: "text-blue-600" };
if (score >= 40) return { label: "Intermediário", color: "text-yellow-600" };
return { label: "Inicial", color: "text-red-600" };
};
const { data: pdcaActions = [] } = useQuery<PcPdcaAction[]>({
queryKey: ["/api/compass/pdca", selectedPdcaId, "actions"],
queryFn: async () => {
if (!selectedPdcaId) return [];
const res = await fetch(`/api/compass/pdca/${selectedPdcaId}/actions`, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
enabled: !!selectedPdcaId,
});
const { data: requirements = [] } = useQuery<PcRequirement[]>({
queryKey: ["/api/compass/requirements", selectedProjectId],
queryFn: async () => {
const url = selectedProjectId
? `/api/compass/requirements?projectId=${selectedProjectId}`
: `/api/compass/requirements`;
const res = await fetch(url, { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
});
const createTenantMutation = useMutation({
mutationFn: async (data: { name: string; slug?: string }) => {
const res = await fetch("/api/compass/tenants", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar organização");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/tenants"] });
queryClient.invalidateQueries({ queryKey: ["/api/compass/stats"] });
setShowNewTenantDialog(false);
},
});
const { data: crmPartners = [] } = useQuery<any[]>({
queryKey: ["/api/crm/partners"],
queryFn: async () => {
const res = await fetch("/api/crm/partners", { credentials: "include" });
if (!res.ok) return [];
return res.json();
},
});
const createClientMutation = useMutation({
mutationFn: async (data: any) => {
const res = await fetch("/api/crm/clients", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar cliente");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/clients"] });
queryClient.invalidateQueries({ queryKey: ["/api/crm/clients"] });
queryClient.invalidateQueries({ queryKey: ["/api/compass/stats"] });
setShowNewClientDialog(false);
},
});
const createProjectMutation = useMutation({
mutationFn: async (data: { name: string; clientId: number; description?: string; status?: string; projectType?: string }) => {
const res = await fetch("/api/compass/projects", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar projeto");
return res.json();
},
onSuccess: (createdProject: PcProject) => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects"] });
queryClient.invalidateQueries({ queryKey: ["/api/compass/stats"] });
setShowNewProjectDialog(false);
setSelectedProjectId(createdProject.id);
setProjectDetailTab("overview");
setActiveTab("project-detail");
},
});
const updateProjectMutation = useMutation({
mutationFn: async (data: { id: number; name?: string; description?: string; status?: string }) => {
const res = await fetch(`/api/compass/projects/${data.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao atualizar projeto");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects"] });
toast.success("Projeto atualizado com sucesso");
},
});
const deleteProjectMutation = useMutation({
mutationFn: async (projectId: number) => {
const res = await fetch(`/api/compass/projects/${projectId}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao excluir projeto");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects"] });
queryClient.invalidateQueries({ queryKey: ["/api/compass/stats"] });
toast.success("Projeto excluído com sucesso");
},
});
const createLeadMutation = useMutation({
mutationFn: async (data: { name: string; email?: string; phone?: string; company?: string; source?: string }) => {
const res = await fetch("/api/compass/crm/leads", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar lead");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/crm/leads"] });
queryClient.invalidateQueries({ queryKey: ["/api/compass/stats"] });
setShowNewLeadDialog(false);
},
});
const upsertCanvasBlockMutation = useMutation({
mutationFn: async (data: { id?: number; blockType: string; level: string; title?: string; content?: string; notes?: string; score?: number; synthesis?: string }) => {
if (data.id) {
const res = await fetch(`/api/compass/canvas/${data.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao atualizar bloco");
return res.json();
} else {
const res = await fetch(`/api/compass/projects/${selectedProjectId}/canvas`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar bloco");
return res.json();
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "canvas"] });
setEditingBlock(null);
},
});
const addCanvasQuestionMutation = useMutation({
mutationFn: async (data: { question: string; orderIndex: number }) => {
if (!editingBlock?.id) throw new Error("Bloco não selecionado");
const res = await fetch(`/api/compass/canvas/${editingBlock.id}/questions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar pergunta");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/canvas", editingBlock?.id, "questions"] });
setNewQuestionText("");
},
});
const updateCanvasQuestionMutation = useMutation({
mutationFn: async (data: { id: number; answer?: string; score?: number }) => {
const res = await fetch(`/api/compass/canvas/questions/${data.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ answer: data.answer, score: data.score }),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao atualizar pergunta");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/canvas", editingBlock?.id, "questions"] });
},
});
const deleteCanvasQuestionMutation = useMutation({
mutationFn: async (id: number) => {
const res = await fetch(`/api/compass/canvas/questions/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao excluir pergunta");
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/canvas", editingBlock?.id, "questions"] });
},
});
const addCanvasOutputMutation = useMutation({
mutationFn: async (data: { description: string; orderIndex: number }) => {
if (!editingBlock?.id) throw new Error("Bloco não selecionado");
const res = await fetch(`/api/compass/canvas/${editingBlock.id}/outputs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar saída");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/canvas", editingBlock?.id, "outputs"] });
setNewOutputText("");
},
});
const deleteCanvasOutputMutation = useMutation({
mutationFn: async (id: number) => {
const res = await fetch(`/api/compass/canvas/outputs/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao excluir saída");
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/canvas", editingBlock?.id, "outputs"] });
},
});
const addTeamMemberMutation = useMutation({
mutationFn: async (data: { name: string; email?: string; role: string }) => {
if (!selectedProjectId) throw new Error("Projeto não selecionado");
const res = await fetch(`/api/compass/projects/${selectedProjectId}/team`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao adicionar membro");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "team"] });
setShowTeamMemberDialog(false);
toast.success("Membro adicionado com sucesso");
},
onError: () => {
toast.error("Erro ao adicionar membro");
},
});
const removeTeamMemberMutation = useMutation({
mutationFn: async (id: number) => {
const res = await fetch(`/api/compass/team/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao remover membro");
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "team"] });
toast.success("Membro removido");
},
});
const addProjectTaskMutation = useMutation({
mutationFn: async (data: { title: string; description?: string; priority: string; assignedTo?: string; dueDate?: string }) => {
if (!selectedProjectId) throw new Error("Projeto não selecionado");
const res = await fetch(`/api/compass/projects/${selectedProjectId}/tasks`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar tarefa");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "tasks"] });
setShowNewTaskDialog(false);
toast.success("Tarefa criada com sucesso");
},
onError: () => {
toast.error("Erro ao criar tarefa");
},
});
const updateProjectTaskMutation = useMutation({
mutationFn: async ({ id, data }: { id: number; data: Partial<PcProjectTask> }) => {
const res = await fetch(`/api/compass/tasks/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao atualizar tarefa");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "tasks"] });
},
});
const uploadProjectFileMutation = useMutation({
mutationFn: async (file: File) => {
if (!selectedProjectId) throw new Error("Projeto não selecionado");
const formData = new FormData();
formData.append("file", file);
const res = await fetch(`/api/compass/projects/${selectedProjectId}/files`, {
method: "POST",
body: formData,
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao fazer upload");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "files"] });
setShowFileUploadDialog(false);
toast.success("Arquivo enviado com sucesso");
},
onError: () => {
toast.error("Erro ao enviar arquivo");
},
});
const deleteProjectFileMutation = useMutation({
mutationFn: async (id: number) => {
const res = await fetch(`/api/compass/files/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao excluir arquivo");
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "files"] });
toast.success("Arquivo excluído");
},
});
const saveProjectHistoryMutation = useMutation({
mutationFn: async (content: string) => {
if (!selectedProjectId) throw new Error("Projeto não selecionado");
const res = await fetch(`/api/compass/projects/${selectedProjectId}/history`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao salvar histórico");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "history"] });
toast.success("Histórico salvo com sucesso");
setHistoryIsSaving(false);
},
onError: () => {
toast.error("Erro ao salvar histórico");
setHistoryIsSaving(false);
},
});
const createProcessMutation = useMutation({
mutationFn: async (data: { name: string; description?: string; category?: string; owner?: string }) => {
const res = await fetch(`/api/compass/projects/${selectedProjectId}/processes`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar processo");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "processes"] });
setShowNewProcessDialog(false);
},
});
const createSwotAnalysisMutation = useMutation({
mutationFn: async (data: { name: string; description?: string; sector?: string }) => {
const res = await fetch(`/api/compass/projects/${selectedProjectId}/swot`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar análise SWOT");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "swot"] });
setShowNewSwotDialog(false);
},
});
const createSwotItemMutation = useMutation({
mutationFn: async (data: { type: string; description: string; impact?: string; actionPlan?: string }) => {
const res = await fetch(`/api/compass/swot/${selectedSwotId}/items`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar item SWOT");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/swot", selectedSwotId, "items"] });
},
});
const updateSwotItemMutation = useMutation({
mutationFn: async ({ id, data }: { id: number; data: Partial<PcSwotItem> }) => {
const res = await fetch(`/api/compass/swot-items/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao atualizar item SWOT");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/swot", selectedSwotId, "items"] });
setEditingSwotItem(null);
toast.success("Item SWOT atualizado com sucesso");
},
onError: (error: Error) => {
toast.error(`Erro ao atualizar: ${error.message}`);
},
});
const deleteSwotItemMutation = useMutation({
mutationFn: async (id: number) => {
const res = await fetch(`/api/compass/swot-items/${id}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao excluir item SWOT");
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/swot", selectedSwotId, "items"] });
toast.success("Item SWOT excluído com sucesso");
},
onError: (error: Error) => {
toast.error(`Erro ao excluir: ${error.message}`);
},
});
const createPdcaCycleMutation = useMutation({
mutationFn: async (data: { title: string; description?: string; projectId?: number; priority?: string }) => {
const res = await fetch("/api/compass/pdca", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar ciclo PDCA");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/pdca"] });
setShowNewPdcaDialog(false);
},
});
const createPdcaActionMutation = useMutation({
mutationFn: async (data: { phase: string; title: string; description?: string; responsible?: string }) => {
const res = await fetch(`/api/compass/pdca/${selectedPdcaId}/actions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar ação PDCA");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/pdca", selectedPdcaId, "actions"] });
},
});
const createRequirementMutation = useMutation({
mutationFn: async (data: { title: string; description?: string; type?: string; priority?: string; projectId?: number }) => {
const res = await fetch("/api/compass/requirements", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Falha ao criar requisito");
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/compass/requirements"] });
setShowNewRequirementDialog(false);
setNewReqTitle("");
setNewReqDescription("");
setNewReqType("functional");
setNewReqPriority("medium");
},
});
const filteredClients = clients.filter(c =>
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.company?.toLowerCase().includes(searchTerm.toLowerCase())
);
const filteredProjects = projects.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase())
);
const renderDashboard = () => (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card className="border-l-4 border-l-blue-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<Users className="h-4 w-4" />
Clientes
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.clients || 0}</div>
<p className="text-xs text-muted-foreground">clientes cadastrados</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-purple-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<FolderKanban className="h-4 w-4" />
Projetos
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.projects || 0}</div>
<p className="text-xs text-muted-foreground">{stats?.activeProjects || 0} em andamento</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-yellow-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<UserPlus className="h-4 w-4" />
Leads
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.leads || 0}</div>
<p className="text-xs text-muted-foreground">leads no funil</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-green-500">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground flex items-center gap-2">
<TrendingUp className="h-4 w-4" />
Oportunidades
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{stats?.opportunities || 0}</div>
<p className="text-xs text-muted-foreground">{stats?.openOpportunities || 0} abertas</p>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg">Projetos Recentes</CardTitle>
<Button variant="ghost" size="sm" onClick={() => setActiveTab("projects")}>
Ver todos <ChevronRight className="h-4 w-4 ml-1" />
</Button>
</CardHeader>
<CardContent>
{projectsLoading ? (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin" />
</div>
) : projects.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<FolderKanban className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Nenhum projeto ainda</p>
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={() => setShowNewProjectDialog(true)}
>
<Plus className="h-4 w-4 mr-1" /> Criar Projeto
</Button>
</div>
) : (
<div className="space-y-3">
{projects.slice(0, 5).map(project => (
<div
key={project.id}
className="flex items-center justify-between p-3 rounded-lg border hover:bg-muted/50 cursor-pointer"
data-testid={`project-item-${project.id}`}
onClick={() => {
setSelectedProjectId(project.id);
setProjectDetailTab("overview");
setActiveTab("project-detail");
}}
>
<div>
<div className="flex items-center gap-2">
<p className="font-medium">{project.name}</p>
{project.projectType && project.projectType !== "consultoria" && (
<Badge variant="outline" className={
project.projectType === "valuation" ? "border-emerald-500 text-emerald-600" :
project.projectType === "programacao" ? "border-purple-500 text-purple-600" : ""
}>
{project.projectType === "valuation" ? "Valuation" :
project.projectType === "programacao" ? "Dev" : project.projectType}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground">
{clients.find(c => c.id === project.clientId)?.name || "Cliente"}
</p>
</div>
<Badge className={statusColors[project.status] || "bg-gray-100"}>
{statusLabels[project.status] || project.status}
</Badge>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-lg">CRM e Vendas</CardTitle>
<a href="/crm">
<Button variant="ghost" size="sm">
Abrir CRM <ChevronRight className="h-4 w-4 ml-1" />
</Button>
</a>
</CardHeader>
<CardContent>
<div className="text-center py-8 text-muted-foreground">
<TrendingUp className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Gerencie leads, oportunidades e clientes</p>
<a href="/crm">
<Button
variant="outline"
size="sm"
className="mt-2"
>
<ExternalLink className="h-4 w-4 mr-1" /> Acessar Arcádia CRM
</Button>
</a>
</div>
</CardContent>
</Card>
</div>
{/* Integration Links Section */}
<Card className="bg-gradient-to-r from-indigo-50 to-purple-50 border-indigo-200">
<CardHeader className="pb-2">
<CardTitle className="text-lg flex items-center gap-2">
<ExternalLink className="h-5 w-5 text-indigo-600" />
Integrações e Ferramentas
</CardTitle>
<CardDescription>Acesse módulos complementares do Arcádia</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<a href="/insights" className="block p-4 bg-white rounded-lg border hover:shadow-md transition-shadow group" data-testid="link-insights">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center group-hover:bg-blue-200 transition-colors">
<BarChart3 className="h-5 w-5 text-blue-600" />
</div>
<div>
<p className="font-medium group-hover:text-blue-600 transition-colors">Arcádia Insights</p>
<p className="text-xs text-muted-foreground">Dashboards e análises BI</p>
</div>
</div>
</a>
<a href="/" className="block p-4 bg-white rounded-lg border hover:shadow-md transition-shadow group" data-testid="link-erp">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-green-100 flex items-center justify-center group-hover:bg-green-200 transition-colors">
<Network className="h-5 w-5 text-green-600" />
</div>
<div>
<p className="font-medium group-hover:text-green-600 transition-colors">Conexões ERP</p>
<p className="text-xs text-muted-foreground">Integrações de sistemas</p>
</div>
</div>
</a>
<a href="/agent" className="block p-4 bg-white rounded-lg border hover:shadow-md transition-shadow group" data-testid="link-agent">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-amber-100 flex items-center justify-center group-hover:bg-amber-200 transition-colors">
<Lightbulb className="h-5 w-5 text-amber-600" />
</div>
<div>
<p className="font-medium group-hover:text-amber-600 transition-colors">Arcádia Agent</p>
<p className="text-xs text-muted-foreground">Assistente IA empresarial</p>
</div>
</div>
</a>
</div>
</CardContent>
</Card>
{tenants.length === 0 && (
<Card className="border-dashed border-2">
<CardContent className="flex flex-col items-center justify-center py-12">
<Building2 className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">Configure sua Organização</h3>
<p className="text-muted-foreground text-center mb-4 max-w-md">
Para começar a usar o Process Compass, você precisa criar uma organização (consultoria) primeiro.
</p>
<Button onClick={() => setShowNewTenantDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Criar Organização
</Button>
</CardContent>
</Card>
)}
</div>
);
const renderClients = () => (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar clientes..."
className="pl-9"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
data-testid="search-clients"
/>
</div>
<Button onClick={() => setShowNewClientDialog(true)} data-testid="btn-new-client">
<Plus className="h-4 w-4 mr-2" /> Novo Cliente
</Button>
</div>
{clientsLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
) : filteredClients.length === 0 ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-12">
<Users className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Nenhum cliente encontrado</h3>
<p className="text-muted-foreground text-center mb-4">
{searchTerm ? "Tente uma busca diferente" : "Adicione seu primeiro cliente para começar"}
</p>
{!searchTerm && (
<Button onClick={() => setShowNewClientDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Adicionar Cliente
</Button>
)}
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredClients.map(client => (
<Card key={client.id} className="hover:shadow-md transition-shadow cursor-pointer" data-testid={`client-card-${client.id}`}>
<CardHeader className="pb-2">
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<Building2 className="h-5 w-5 text-primary" />
</div>
<div>
<CardTitle className="text-base">{client.name}</CardTitle>
{client.company && (
<CardDescription>{client.company}</CardDescription>
)}
</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>
<Eye className="h-4 w-4 mr-2" /> Ver Detalhes
</DropdownMenuItem>
<DropdownMenuItem>
<Edit className="h-4 w-4 mr-2" /> Editar
</DropdownMenuItem>
<DropdownMenuItem className="text-destructive">
<Trash2 className="h-4 w-4 mr-2" /> Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</CardHeader>
<CardContent>
<div className="space-y-2 text-sm">
{client.email && (
<div className="flex items-center gap-2 text-muted-foreground">
<Mail className="h-4 w-4" />
<span className="truncate">{client.email}</span>
</div>
)}
{client.phone && (
<div className="flex items-center gap-2 text-muted-foreground">
<Phone className="h-4 w-4" />
<span>{client.phone}</span>
</div>
)}
{client.industry && (
<div className="flex items-center gap-2 text-muted-foreground">
<Briefcase className="h-4 w-4" />
<span>{client.industry}</span>
</div>
)}
</div>
<div className="mt-3 pt-3 border-t flex items-center justify-between">
<Badge className={statusColors[client.status] || "bg-gray-100"}>
{statusLabels[client.status] || client.status}
</Badge>
<span className="text-xs text-muted-foreground">
{projects.filter(p => p.clientId === client.id).length} projetos
</span>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
const renderProjects = () => (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar projetos..."
className="pl-9"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
data-testid="search-projects"
/>
</div>
<Button onClick={() => setShowNewProjectDialog(true)} data-testid="btn-new-project">
<Plus className="h-4 w-4 mr-2" /> Novo Projeto
</Button>
</div>
{projectsLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
) : filteredProjects.length === 0 ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-12">
<FolderKanban className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-lg font-semibold mb-2">Nenhum projeto encontrado</h3>
<p className="text-muted-foreground text-center mb-4">
{clients.length === 0
? "Adicione um cliente primeiro para criar projetos"
: searchTerm ? "Tente uma busca diferente" : "Crie seu primeiro projeto de consultoria"}
</p>
{clients.length > 0 && !searchTerm && (
<Button onClick={() => setShowNewProjectDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Criar Projeto
</Button>
)}
</CardContent>
</Card>
) : (
<div className="space-y-3">
{filteredProjects.map(project => {
const client = clients.find(c => c.id === project.clientId);
return (
<Card
key={project.id}
className="hover:shadow-md transition-shadow cursor-pointer"
data-testid={`project-card-${project.id}`}
onClick={() => {
setSelectedProjectId(project.id);
setProjectDetailTab("overview");
setActiveTab("project-detail");
}}
>
<CardContent className="p-4">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<div className="h-12 w-12 rounded-lg bg-primary/10 flex items-center justify-center">
<FolderKanban className="h-6 w-6 text-primary" />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="font-semibold text-lg">{project.name}</h3>
{project.projectType && project.projectType !== "consultoria" && (
<Badge variant="outline" className={
project.projectType === "valuation" ? "border-emerald-500 text-emerald-600" :
project.projectType === "programacao" ? "border-purple-500 text-purple-600" : ""
}>
{project.projectType === "valuation" ? "Valuation" :
project.projectType === "programacao" ? "Dev" : project.projectType}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground flex items-center gap-1">
<Building2 className="h-3 w-3" />
{client?.name || "Cliente"}
</p>
{project.description && (
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
{project.description}
</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Badge className={statusColors[project.status] || "bg-gray-100"}>
{statusLabels[project.status] || project.status}
</Badge>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreVertical className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" onClick={(e) => e.stopPropagation()}>
<DropdownMenuItem onClick={() => { setSelectedProjectId(project.id); setActiveTab("canvas"); }}>
<Target className="h-4 w-4 mr-2" /> Canvas
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { setSelectedProjectId(project.id); setActiveTab("processes"); }}>
<Network className="h-4 w-4 mr-2" /> Processos
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { setSelectedProjectId(project.id); setActiveTab("swot"); }}>
<Layers className="h-4 w-4 mr-2" /> SWOT
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { setSelectedProjectId(project.id); setActiveTab("pdca"); }}>
<RefreshCw className="h-4 w-4 mr-2" /> PDCA
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => { setEditingProject(project); setShowEditProjectDialog(true); }}>
<Pencil className="h-4 w-4 mr-2" /> Editar
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive focus:text-destructive"
onClick={() => { setProjectToDelete(project); setShowDeleteProjectConfirm(true); }}
>
<Trash2 className="h-4 w-4 mr-2" /> Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{(project.startDate || project.dueDate) && (
<div className="mt-3 pt-3 border-t flex items-center gap-4 text-xs text-muted-foreground">
{project.startDate && (
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
Início: {new Date(project.startDate).toLocaleDateString('pt-BR')}
</span>
)}
{project.dueDate && (
<span className="flex items-center gap-1">
<Clock className="h-3 w-3" />
Prazo: {new Date(project.dueDate).toLocaleDateString('pt-BR')}
</span>
)}
</div>
)}
</CardContent>
</Card>
);
})}
</div>
)}
</div>
);
const renderProjectDetail = () => {
const project = projects.find(p => p.id === selectedProjectId);
if (!project) return null;
const client = clients.find(c => c.id === project.clientId);
const projectCanvasBlocks = canvasBlocks.filter(b => b.projectId === selectedProjectId);
const projectProcesses = processes.filter(p => p.projectId === selectedProjectId);
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="icon"
onClick={() => { setActiveTab("projects"); setSelectedProjectId(null); }}
data-testid="btn-back-projects"
>
<ChevronRight className="h-5 w-5 rotate-180" />
</Button>
<div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{project.name}</h1>
<Badge className={statusColors[project.status] || "bg-gray-100"}>
{statusLabels[project.status] || project.status}
</Badge>
{project.projectType && project.projectType !== "consultoria" && (
<Badge variant="outline" className={
project.projectType === "valuation" ? "border-emerald-500 text-emerald-600" :
project.projectType === "programacao" ? "border-purple-500 text-purple-600" : ""
}>
{project.projectType === "valuation" ? "Valuation" :
project.projectType === "programacao" ? "Dev" : project.projectType}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground flex items-center gap-1 mt-1">
<Building2 className="h-3 w-3" />
{client?.name || "Cliente"}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => { setEditingProject(project); setShowEditProjectDialog(true); }}>
<Pencil className="h-4 w-4 mr-2" /> Editar
</Button>
<Button onClick={() => setProjectDetailTab("canvas")}>
<Target className="h-4 w-4 mr-2" /> Abrir Canvas
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
<Target className="h-5 w-5 text-blue-600" />
</div>
<div>
<p className="text-2xl font-bold">{projectCanvasBlocks.length}</p>
<p className="text-sm text-muted-foreground">Blocos Canvas</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-emerald-100 flex items-center justify-center">
<Network className="h-5 w-5 text-emerald-600" />
</div>
<div>
<p className="text-2xl font-bold">{projectProcesses.length}</p>
<p className="text-sm text-muted-foreground">Processos</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-amber-100 flex items-center justify-center">
<ClipboardList className="h-5 w-5 text-amber-600" />
</div>
<div>
<p className="text-2xl font-bold">0</p>
<p className="text-sm text-muted-foreground">Tarefas</p>
</div>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-red-100 flex items-center justify-center">
<Calendar className="h-5 w-5 text-red-600" />
</div>
<div>
<p className="text-lg font-medium">{project.dueDate ? new Date(project.dueDate).toLocaleDateString('pt-BR') : "Sem prazo"}</p>
<p className="text-sm text-muted-foreground">Data limite</p>
</div>
</div>
</Card>
</div>
{/* Tabs */}
<Tabs value={projectDetailTab} onValueChange={setProjectDetailTab}>
<TabsList className="flex-wrap">
<TabsTrigger value="overview">Visão Geral</TabsTrigger>
<TabsTrigger value="history">História</TabsTrigger>
<TabsTrigger value="team">Equipe</TabsTrigger>
<TabsTrigger value="canvas">Canvas</TabsTrigger>
<TabsTrigger value="processes">Processos</TabsTrigger>
<TabsTrigger value="tasks">Tarefas</TabsTrigger>
<TabsTrigger value="files">Arquivos</TabsTrigger>
<TabsTrigger value="reports">Relatórios</TabsTrigger>
{project.projectType === "valuation" && (
<TabsTrigger value="valuation">Valuation</TabsTrigger>
)}
</TabsList>
<TabsContent value="overview" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Sobre o Projeto</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label className="text-sm text-muted-foreground">Descrição</Label>
<p className="mt-1">{project.description || "Nenhuma descrição adicionada"}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-sm text-muted-foreground">Data de Início</Label>
<p className="mt-1">{project.startDate ? new Date(project.startDate).toLocaleDateString('pt-BR') : "Não definida"}</p>
</div>
<div>
<Label className="text-sm text-muted-foreground">Data Limite</Label>
<p className="mt-1">{project.dueDate ? new Date(project.dueDate).toLocaleDateString('pt-BR') : "Não definida"}</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="history" className="mt-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>História do Projeto</CardTitle>
<Button
onClick={() => {
setHistoryIsSaving(true);
saveProjectHistoryMutation.mutate(projectHistoryContent);
}}
disabled={historyIsSaving}
data-testid="btn-save-history"
>
{historyIsSaving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
Salvar
</Button>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-3">
Documente a história, evolução e decisões importantes do projeto. Use formatação rica, adicione imagens, links e vídeos.
</p>
<RichTextEditor
content={projectHistoryContent}
onChange={setProjectHistoryContent}
placeholder="Escreva a história do projeto aqui..."
className="min-h-[300px]"
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="team" className="mt-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Equipe do Projeto</CardTitle>
<Button onClick={() => setShowTeamMemberDialog(true)} data-testid="btn-add-team-member">
<Plus className="h-4 w-4 mr-2" /> Adicionar Membro
</Button>
</CardHeader>
<CardContent>
{projectTeamMembers.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<Users className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="font-semibold mb-2">Nenhum membro na equipe</h3>
<p className="text-muted-foreground text-center">Adicione membros para gerenciar a equipe do projeto</p>
</div>
) : (
<div className="space-y-3">
{projectTeamMembers.map(member => (
<div key={member.id} className="flex items-center justify-between p-3 border rounded-lg" data-testid={`team-member-${member.id}`}>
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center">
<Users className="h-5 w-5 text-primary" />
</div>
<div>
<p className="font-medium">{member.name}</p>
<p className="text-sm text-muted-foreground">{member.role}</p>
</div>
</div>
<div className="flex items-center gap-2">
{member.email && (
<span className="text-sm text-muted-foreground">{member.email}</span>
)}
<Button
variant="ghost"
size="sm"
onClick={() => removeTeamMemberMutation.mutate(member.id)}
data-testid={`btn-remove-member-${member.id}`}
>
<UserX className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="canvas" className="mt-4">
<div className="space-y-4">
<div className="flex flex-wrap gap-2 mb-4">
{CANVAS_LEVELS.map(level => (
<Button
key={level.id}
variant={selectedCanvasLevel === level.id ? "default" : "outline"}
onClick={() => setSelectedCanvasLevel(level.id)}
className="flex-1 min-w-[120px]"
data-testid={`btn-level-${level.id}`}
>
<div className="text-center">
<div className="font-medium text-sm">{level.label}</div>
<div className="text-xs opacity-70 hidden sm:block">{level.description}</div>
</div>
</Button>
))}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
{CANVAS_BLOCK_TYPES.map(blockType => {
const block = projectCanvasBlocks.find(b => b.blockType === blockType.id && b.level === selectedCanvasLevel);
return (
<Card
key={blockType.id}
className={`cursor-pointer hover:shadow-md transition-shadow border-2 ${blockType.color}`}
onClick={() => {
if (block) {
setEditingBlock(block);
} else {
setEditingBlock({ id: 0, projectId: selectedProjectId!, blockType: blockType.id, level: selectedCanvasLevel, title: null, content: null, notes: null, score: 0, synthesis: null });
}
}}
data-testid={`canvas-block-${blockType.id}`}
>
<CardHeader className="p-3">
<CardTitle className="text-sm">{blockType.label}</CardTitle>
</CardHeader>
<CardContent className="p-3 pt-0">
{block ? (
<div className="space-y-1">
{block.title && <p className="font-medium text-sm">{block.title}</p>}
<p className="text-xs text-muted-foreground line-clamp-3">{block.content || "Clique para adicionar"}</p>
<div className="flex items-center gap-1 mt-2">
<div className="h-1 flex-1 bg-gray-200 rounded">
<div className="h-1 bg-primary rounded" style={{ width: `${block.score}%` }} />
</div>
<span className="text-xs">{block.score}%</span>
</div>
</div>
) : (
<p className="text-xs text-muted-foreground">Clique para adicionar</p>
)}
</CardContent>
</Card>
);
})}
</div>
</div>
</TabsContent>
<TabsContent value="processes" className="mt-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Processos</CardTitle>
<Button onClick={() => setShowNewProcessDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Novo Processo
</Button>
</CardHeader>
<CardContent>
{projectProcesses.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<Network className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="font-semibold mb-2">Nenhum processo mapeado</h3>
<p className="text-muted-foreground text-center">Comece a mapear os processos do projeto</p>
</div>
) : (
<div className="space-y-3">
{projectProcesses.map(process => (
<Card key={process.id} className="p-4" data-testid={`card-process-${process.id}`}>
<div className="flex items-center justify-between">
<div className="flex-1">
<p className="font-medium">{process.name}</p>
<p className="text-sm text-muted-foreground">{process.category || "Sem categoria"}</p>
</div>
<div className="flex items-center gap-2">
<Badge>{process.status}</Badge>
<Button
size="sm"
variant="outline"
onClick={() => {
setDiagramProcessId(process.id);
setShowProcessDiagramDialog(true);
}}
data-testid={`btn-diagram-${process.id}`}
>
<Network className="h-4 w-4 mr-1" /> Diagrama
</Button>
</div>
</div>
</Card>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="tasks" className="mt-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Tarefas</CardTitle>
<Button onClick={() => setShowNewTaskDialog(true)} data-testid="btn-new-task">
<Plus className="h-4 w-4 mr-2" /> Nova Tarefa
</Button>
</CardHeader>
<CardContent>
{projectTasks.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<ClipboardList className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="font-semibold mb-2">Nenhuma tarefa</h3>
<p className="text-muted-foreground text-center">Adicione tarefas para acompanhar o progresso</p>
</div>
) : (
<div className="space-y-3">
{projectTasks.map(task => (
<div key={task.id} className="flex items-center justify-between p-3 border rounded-lg" data-testid={`task-${task.id}`}>
<div className="flex items-center gap-3">
<Checkbox
checked={task.status === "completed"}
onCheckedChange={(checked) => {
updateProjectTaskMutation.mutate({
id: task.id,
data: { status: checked ? "completed" : "pending" }
});
}}
data-testid={`checkbox-task-${task.id}`}
/>
<div>
<p className={`font-medium ${task.status === "completed" ? "line-through text-muted-foreground" : ""}`}>
{task.title}
</p>
{task.description && (
<p className="text-sm text-muted-foreground">{task.description}</p>
)}
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant={task.priority === "high" ? "destructive" : task.priority === "medium" ? "default" : "secondary"}>
{task.priority === "high" ? "Alta" : task.priority === "medium" ? "Média" : "Baixa"}
</Badge>
{task.assignedTo && (
<span className="text-sm text-muted-foreground">{task.assignedTo}</span>
)}
{task.dueDate && (
<span className="text-xs text-muted-foreground">
{new Date(task.dueDate).toLocaleDateString('pt-BR')}
</span>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="files" className="mt-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Arquivos</CardTitle>
<label>
<input
type="file"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
uploadProjectFileMutation.mutate(file);
}
}}
data-testid="input-file-upload"
/>
<Button asChild>
<span>
{uploadProjectFileMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Upload className="h-4 w-4 mr-2" />
)}
Upload
</span>
</Button>
</label>
</CardHeader>
<CardContent>
{projectFiles.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<FileText className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="font-semibold mb-2">Nenhum arquivo</h3>
<p className="text-muted-foreground text-center">Faça upload de arquivos relacionados ao projeto</p>
</div>
) : (
<div className="space-y-3">
{projectFiles.map(file => (
<div key={file.id} className="flex items-center justify-between p-3 border rounded-lg" data-testid={`file-${file.id}`}>
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">{file.originalName}</p>
<p className="text-xs text-muted-foreground">
{(file.size / 1024).toFixed(1)} KB - {new Date(file.createdAt).toLocaleDateString('pt-BR')}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={() => window.open(file.url, '_blank')} data-testid={`btn-download-${file.id}`}>
<Download className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => deleteProjectFileMutation.mutate(file.id)}
data-testid={`btn-delete-file-${file.id}`}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="reports" className="mt-4">
<div className="space-y-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Relatórios do Projeto</CardTitle>
<Button onClick={() => setShowReportConfigModal(true)} data-testid="btn-new-report">
<Plus className="h-4 w-4 mr-2" /> Gerar Relatório
</Button>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4 mb-6">
<Card className="p-4 cursor-pointer hover:border-primary transition-colors" onClick={() => handleQuickReport("executive_summary")}>
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-blue-100 flex items-center justify-center">
<FileText className="h-5 w-5 text-blue-600" />
</div>
<div>
<p className="font-medium">Sumário Executivo</p>
<p className="text-xs text-muted-foreground">Visão geral rápida</p>
</div>
</div>
</Card>
<Card className="p-4 cursor-pointer hover:border-primary transition-colors" onClick={() => handleQuickReport("full_diagnostic")}>
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-green-100 flex items-center justify-center">
<ClipboardList className="h-5 w-5 text-green-600" />
</div>
<div>
<p className="font-medium">Diagnóstico Completo</p>
<p className="text-xs text-muted-foreground">Todos os dados</p>
</div>
</div>
</Card>
<Card className="p-4 cursor-pointer hover:border-primary transition-colors" onClick={() => handleQuickReport("swot_report")}>
<div className="flex items-center gap-3">
<div className="h-10 w-10 rounded-lg bg-purple-100 flex items-center justify-center">
<LayoutGrid className="h-5 w-5 text-purple-600" />
</div>
<div>
<p className="font-medium">Análise SWOT</p>
<p className="text-xs text-muted-foreground">Forças e fraquezas</p>
</div>
</div>
</Card>
</div>
<h4 className="font-medium mb-3">Relatórios Gerados</h4>
{!generatedReports?.length ? (
<div className="text-center py-8 text-muted-foreground">
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Nenhum relatório gerado ainda</p>
<p className="text-sm">Clique em "Gerar Relatório" para criar um novo</p>
</div>
) : (
<div className="space-y-2">
{generatedReports.map((report: any) => (
<div key={report.id} className="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50">
<div className="flex items-center gap-3">
<FileText className="h-5 w-5 text-muted-foreground" />
<div>
<p className="font-medium">{report.name}</p>
<p className="text-xs text-muted-foreground">
{new Date(report.createdAt).toLocaleDateString('pt-BR')} - {report.format?.toUpperCase()}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{report.status === "completed" && report.fileUrl && (
<Button variant="outline" size="sm" onClick={() => window.open(report.fileUrl, '_blank')}>
<Download className="h-4 w-4 mr-1" /> Baixar
</Button>
)}
{report.status === "generating" && (
<Badge variant="secondary"><Loader2 className="h-3 w-3 mr-1 animate-spin" /> Gerando...</Badge>
)}
{report.status === "failed" && (
<Badge variant="destructive">Falhou</Badge>
)}
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</TabsContent>
{project.projectType === "valuation" && (
<TabsContent value="valuation" className="mt-4">
<div className="space-y-4">
<Tabs value={activeValuationSubTab} onValueChange={setActiveValuationSubTab}>
<TabsList className="flex-wrap">
<TabsTrigger value="overview">Visão Geral</TabsTrigger>
<TabsTrigger value="checklist">Checklist</TabsTrigger>
<TabsTrigger value="sector">Análise Setorial</TabsTrigger>
</TabsList>
<TabsContent value="overview" className="mt-4">
<div className="grid grid-cols-3 gap-4">
<Card className="p-4">
<p className="text-sm text-muted-foreground">Checklist</p>
<p className="text-2xl font-bold">
{Array.isArray(valuationChecklist) ? valuationChecklist.filter((c: any) => c.status === "completed").length : 0}/{Array.isArray(valuationChecklist) ? valuationChecklist.length : 0}
</p>
<p className="text-xs text-muted-foreground">itens completos</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">Score Setorial</p>
<p className="text-2xl font-bold">{valuationSectorAnalysis?.latest?.overallScore || "--"}%</p>
<p className="text-xs text-muted-foreground">
{valuationSectorAnalysis?.latest?.overallScore
? getValuationScoreLevel(valuationSectorAnalysis.latest.overallScore).label
: "Não calculado"}
</p>
</Card>
<Card className="p-4 cursor-pointer hover:border-primary transition-colors" onClick={() => navigate("/canvas")}>
<p className="text-sm text-muted-foreground">Canvas</p>
<p className="text-2xl font-bold text-primary">Abrir</p>
<p className="text-xs text-muted-foreground">Módulo Centralizado</p>
</Card>
</div>
</TabsContent>
<TabsContent value="checklist" className="mt-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle>Checklist de Coleta de Dados</CardTitle>
{(!Array.isArray(valuationChecklist) || valuationChecklist.length === 0) && (
<Button
onClick={() => initializeValuationChecklistMutation.mutate()}
disabled={initializeValuationChecklistMutation.isPending}
data-testid="btn-initialize-valuation-checklist"
>
{initializeValuationChecklistMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Plus className="h-4 w-4 mr-2" />
)}
Inicializar Checklist
</Button>
)}
</CardHeader>
<CardContent>
{!Array.isArray(valuationChecklist) || valuationChecklist.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12">
<ClipboardList className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">Clique em "Inicializar Checklist" para começar</p>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center gap-4 p-4 bg-muted/30 rounded-lg">
<div className="flex-1">
<div className="flex justify-between text-sm mb-1">
<span>Progresso</span>
<span>{valuationChecklist.filter((c: any) => c.status === "completed").length} de {valuationChecklist.length}</span>
</div>
<div className="h-2 bg-gray-200 rounded-full">
<div
className="h-2 bg-primary rounded-full transition-all"
style={{ width: `${(valuationChecklist.filter((c: any) => c.status === "completed").length / valuationChecklist.length) * 100}%` }}
/>
</div>
</div>
</div>
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="sector" className="mt-4">
<Card>
<CardHeader>
<CardTitle>Análise Setorial</CardTitle>
</CardHeader>
<CardContent>
{valuationSectorAnalysis?.latest ? (
<div className="space-y-4">
<div className="flex items-center gap-4">
<div className="h-24 w-24 rounded-full border-4 border-primary flex items-center justify-center">
<span className="text-2xl font-bold">{valuationSectorAnalysis.latest.overallScore}%</span>
</div>
<div>
<p className="font-medium">{getValuationScoreLevel(valuationSectorAnalysis.latest.overallScore).label}</p>
<p className="text-sm text-muted-foreground">Score geral da análise setorial</p>
</div>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center py-12">
<TrendingUp className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground">Análise setorial não realizada</p>
</div>
)}
</CardContent>
</Card>
</TabsContent>
</Tabs>
<Card className="bg-muted/30">
<CardContent className="p-4">
<Button onClick={() => { setActiveTab("valuation"); }} variant="outline" className="w-full">
<TrendingUp className="h-4 w-4 mr-2" /> Abrir Módulo Valuation Completo
</Button>
</CardContent>
</Card>
</div>
</TabsContent>
)}
</Tabs>
</div>
);
};
const handleNavClick = (tab: string) => {
setActiveTab(tab);
setSidebarOpen(false);
};
const renderSidebar = () => (
<div className="p-4 space-y-2">
<div className="flex items-center justify-between px-3 py-2 mb-4">
<div className="flex items-center gap-2">
<Compass className="h-6 w-6 text-primary" />
<span className="font-semibold text-lg">Process Compass</span>
</div>
<Button variant="ghost" size="icon" className="md:hidden" onClick={() => setSidebarOpen(false)}>
<X className="h-5 w-5" />
</Button>
</div>
<Button
variant={activeTab === "dashboard" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("dashboard")}
data-testid="nav-dashboard"
>
<LayoutDashboard className="h-4 w-4 mr-2" />
Dashboard
</Button>
<Button
variant={activeTab === "clients" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("clients")}
data-testid="nav-clients"
>
<Users className="h-4 w-4 mr-2" />
Clientes
</Button>
<Button
variant={activeTab === "projects" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("projects")}
data-testid="nav-projects"
>
<FolderKanban className="h-4 w-4 mr-2" />
Projetos
</Button>
<div className="pt-4 border-t mt-4">
<p className="px-3 text-xs font-medium text-muted-foreground mb-2">FERRAMENTAS</p>
<Button
variant={activeTab === "canvas" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("canvas")}
data-testid="nav-canvas"
>
<Target className="h-4 w-4 mr-2" />
Canvas
</Button>
<Button
variant={activeTab === "processes" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("processes")}
data-testid="nav-processes"
>
<Network className="h-4 w-4 mr-2" />
Processos
</Button>
<Button
variant={activeTab === "swot" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("swot")}
data-testid="nav-swot"
>
<Layers className="h-4 w-4 mr-2" />
SWOT
</Button>
<Button
variant={activeTab === "pdca" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("pdca")}
data-testid="nav-pdca"
>
<RefreshCw className="h-4 w-4 mr-2" />
PDCA
</Button>
<Button
variant={activeTab === "requirements" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("requirements")}
data-testid="nav-requirements"
>
<FileCheck className="h-4 w-4 mr-2" />
Requisitos
</Button>
<Button
variant={activeTab === "valuation" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("valuation")}
data-testid="nav-valuation"
>
<TrendingUp className="h-4 w-4 mr-2" />
Valuation
</Button>
</div>
<div className="pt-4 border-t mt-4">
<Button
variant={activeTab === "settings" ? "secondary" : "ghost"}
className="w-full justify-start"
onClick={() => handleNavClick("settings")}
data-testid="nav-settings"
>
<Settings className="h-4 w-4 mr-2" />
Configurações
</Button>
</div>
</div>
);
return (
<BrowserFrame>
<div className="flex h-full bg-background relative">
{/* Mobile sidebar overlay */}
{sidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 md:hidden"
onClick={() => setSidebarOpen(false)}
/>
)}
{/* Sidebar - hidden on mobile, slide-in when open */}
<div className={`
fixed md:relative inset-y-0 left-0 z-50
w-64 border-r bg-background md:bg-muted/30
transform transition-transform duration-200 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full md:translate-x-0'}
`}>
{renderSidebar()}
</div>
<div className="flex-1 flex flex-col overflow-hidden bg-background">
{/* Mobile header */}
<div className="md:hidden flex items-center gap-3 p-4 border-b">
<Button variant="ghost" size="icon" onClick={() => setSidebarOpen(true)} data-testid="btn-mobile-menu">
<Menu className="h-5 w-5" />
</Button>
<div className="flex items-center gap-2">
<Compass className="h-5 w-5 text-primary" />
<span className="font-semibold">Process Compass</span>
</div>
</div>
<div className="flex-1 p-4 md:p-6 overflow-y-auto bg-background min-h-full">
{activeTab === "dashboard" && renderDashboard()}
{activeTab === "clients" && renderClients()}
{activeTab === "projects" && renderProjects()}
{activeTab === "project-detail" && renderProjectDetail()}
{activeTab === "canvas" && (
<TooltipProvider>
<div className="space-y-4">
<Alert className="bg-blue-50 border-blue-200">
<Lightbulb className="h-4 w-4 text-blue-600" />
<AlertDescription className="text-blue-800">
<strong>Dica:</strong> {MODULE_HELP.canvas.tips[0]}
</AlertDescription>
</Alert>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div>
<h2 className="text-xl md:text-2xl font-bold">Canvas de Diagnóstico</h2>
<p className="text-sm md:text-base text-muted-foreground">Business Model Canvas expandido</p>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="font-semibold">{MODULE_HELP.canvas.title}</p>
<p className="text-sm">{MODULE_HELP.canvas.description}</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={() => navigate("/canvas")} data-testid="btn-open-canvas-module">
<ExternalLink className="h-4 w-4 mr-2" />
Abrir Módulo Canvas
</Button>
<Select value={selectedProjectId?.toString() || ""} onValueChange={(v) => setSelectedProjectId(v ? parseInt(v) : null)}>
<SelectTrigger className="w-full md:w-64" data-testid="select-project-canvas">
<SelectValue placeholder="Selecione um projeto" />
</SelectTrigger>
<SelectContent>
{projects.map(p => (
<SelectItem key={p.id} value={p.id.toString()}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{!selectedProjectId ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Target className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">Selecione um Projeto</h3>
<p className="text-muted-foreground text-center max-w-md">
Escolha um projeto acima para acessar o Business Model Canvas.
</p>
</CardContent>
</Card>
) : (
<>
<div className="flex flex-wrap gap-2 mb-4">
{CANVAS_LEVELS.map(level => (
<Button
key={level.id}
variant={selectedCanvasLevel === level.id ? "default" : "outline"}
onClick={() => setSelectedCanvasLevel(level.id)}
className="flex-1 min-w-[120px]"
data-testid={`btn-level-${level.id}`}
>
<div className="text-center">
<div className="font-medium text-sm">{level.label}</div>
<div className="text-xs opacity-70 hidden sm:block">{level.description}</div>
</div>
</Button>
))}
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
{CANVAS_BLOCK_TYPES.map(blockType => {
const block = canvasBlocks.find(b => b.blockType === blockType.id && b.level === selectedCanvasLevel);
return (
<Card
key={blockType.id}
className={`cursor-pointer hover:shadow-md transition-shadow border-2 ${blockType.color}`}
onClick={() => {
if (block) {
setEditingBlock(block);
} else {
setEditingBlock({ id: 0, projectId: selectedProjectId, blockType: blockType.id, level: selectedCanvasLevel, title: null, content: null, notes: null, score: 0, synthesis: null });
}
}}
data-testid={`canvas-block-${blockType.id}`}
>
<CardHeader className="p-3 pb-1">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{block ? (
block.score > 70 ? (
<div className="w-5 h-5 rounded-full bg-green-100 flex items-center justify-center">
<CheckCircle className="h-3 w-3 text-green-600" />
</div>
) : block.score > 30 ? (
<div className="w-5 h-5 rounded-full bg-yellow-100 flex items-center justify-center">
<AlertCircle className="h-3 w-3 text-yellow-600" />
</div>
) : (
<div className="w-5 h-5 rounded-full bg-red-100 flex items-center justify-center">
<AlertCircle className="h-3 w-3 text-red-600" />
</div>
)
) : (
<div className="w-5 h-5 rounded-full bg-gray-100 flex items-center justify-center">
<AlertCircle className="h-3 w-3 text-gray-400" />
</div>
)}
<CardTitle className="text-sm">{blockType.label}</CardTitle>
</div>
{block && (block as any).questionsCount > 0 && (
<Badge variant="outline" className="text-xs">
{(block as any).answeredCount || 0}/{(block as any).questionsCount}
</Badge>
)}
</div>
<p className="text-xs text-muted-foreground mt-1">{blockType.description || ""}</p>
</CardHeader>
<CardContent className="p-3 pt-0">
{block ? (
<div className="space-y-1">
{block.title && <p className="font-medium text-sm">{block.title}</p>}
<p className="text-xs text-muted-foreground line-clamp-2">{block.content || "Clique para adicionar"}</p>
<div className="flex items-center gap-1 mt-2">
<div className="h-1.5 flex-1 bg-gray-200 rounded">
<div
className={`h-1.5 rounded transition-all ${
block.score > 70 ? "bg-green-500" :
block.score > 30 ? "bg-yellow-500" : "bg-red-500"
}`}
style={{ width: `${block.score}%` }}
/>
</div>
</div>
</div>
) : (
<p className="text-xs text-muted-foreground">Clique para adicionar</p>
)}
</CardContent>
</Card>
);
})}
</div>
</>
)}
<Dialog open={!!editingBlock} onOpenChange={(open) => {
if (!open) {
setEditingBlock(null);
setCanvasDialogTab("content");
setNewQuestionText("");
setNewOutputText("");
}
}}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Target className="h-5 w-5" />
{CANVAS_BLOCK_TYPES.find(b => b.id === editingBlock?.blockType)?.label} - {CANVAS_LEVELS.find(l => l.id === editingBlock?.level)?.label}
</DialogTitle>
</DialogHeader>
{(() => {
const questionsWithScores = canvasQuestions.filter(q => q.score !== null);
const avgScore = questionsWithScores.length > 0
? Math.round(questionsWithScores.reduce((sum, q) => sum + (q.score || 0), 0) / questionsWithScores.length * 10)
: 0;
return (
<div className="flex items-center gap-4 px-1 py-2 border-b">
<div className="flex-1">
<div className="text-xs text-muted-foreground mb-1">Maturidade ({questionsWithScores.length}/{canvasQuestions.length} perguntas)</div>
<div className="flex items-center gap-2">
<div className="h-2 flex-1 bg-gray-200 rounded">
<div className="h-2 bg-primary rounded transition-all" style={{ width: `${avgScore}%` }} />
</div>
<span className="text-sm font-medium w-10">{avgScore}%</span>
</div>
</div>
<div className="text-right">
<div className="text-xs text-muted-foreground">Saídas</div>
<span className="text-sm font-medium">{canvasOutputs.filter(o => o.completed).length}/{canvasOutputs.length}</span>
</div>
</div>
);
})()}
<Tabs value={canvasDialogTab} onValueChange={setCanvasDialogTab} className="flex-1 flex flex-col overflow-hidden">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="content">Conteúdo</TabsTrigger>
<TabsTrigger value="questions">Diagnóstico ({canvasQuestions.length})</TabsTrigger>
<TabsTrigger value="outputs">Saídas ({canvasOutputs.length})</TabsTrigger>
</TabsList>
<ScrollArea className="flex-1 pr-4">
<TabsContent value="content" className="mt-4 space-y-4">
<form id="canvas-block-form" onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
upsertCanvasBlockMutation.mutate({
id: editingBlock?.id || undefined,
blockType: editingBlock?.blockType || "",
level: editingBlock?.level || "",
title: formData.get("title") as string,
content: formData.get("content") as string,
notes: formData.get("notes") as string,
synthesis: formData.get("synthesis") as string,
score: parseInt(formData.get("score") as string) || 0,
});
}}>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="block-title">Título</Label>
<Input id="block-title" name="title" defaultValue={editingBlock?.title || ""} placeholder="Título do bloco" data-testid="input-block-title" />
</div>
<div className="space-y-2">
<Label htmlFor="block-content">Descrição</Label>
<Textarea id="block-content" name="content" defaultValue={editingBlock?.content || ""} placeholder="Descreva este elemento do canvas..." rows={4} data-testid="input-block-content" />
</div>
<div className="space-y-2">
<Label htmlFor="block-synthesis" className="flex items-center gap-2">
Síntese Diagnóstica
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-3.5 w-3.5 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs">Resumo consolidado das descobertas e análises para este bloco</p>
</TooltipContent>
</Tooltip>
</Label>
<Textarea id="block-synthesis" name="synthesis" defaultValue={editingBlock?.synthesis || ""} placeholder="Síntese das análises e descobertas..." rows={3} data-testid="input-block-synthesis" />
</div>
<div className="space-y-2">
<Label htmlFor="block-notes">Notas Adicionais</Label>
<Textarea id="block-notes" name="notes" defaultValue={editingBlock?.notes || ""} placeholder="Observações..." rows={2} data-testid="input-block-notes" />
</div>
<input type="hidden" name="score" value={(() => {
const questionsWithScores = canvasQuestions.filter(q => q.score !== null);
return questionsWithScores.length > 0
? Math.round(questionsWithScores.reduce((sum, q) => sum + (q.score || 0), 0) / questionsWithScores.length * 10)
: editingBlock?.score || 0;
})()} />
</div>
</form>
</TabsContent>
<TabsContent value="questions" className="mt-4 space-y-4">
{!editingBlock?.id ? (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Salve o bloco primeiro para adicionar perguntas diagnósticas.
</AlertDescription>
</Alert>
) : (
<>
<div className="flex gap-2">
<Input
placeholder="Nova pergunta diagnóstica..."
value={newQuestionText}
onChange={(e) => setNewQuestionText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && newQuestionText.trim()) {
e.preventDefault();
addCanvasQuestionMutation.mutate({
question: newQuestionText.trim(),
orderIndex: canvasQuestions.length
});
}
}}
data-testid="input-new-question"
/>
<Button
type="button"
disabled={!newQuestionText.trim() || addCanvasQuestionMutation.isPending}
onClick={() => {
if (newQuestionText.trim()) {
addCanvasQuestionMutation.mutate({
question: newQuestionText.trim(),
orderIndex: canvasQuestions.length
});
}
}}
>
{addCanvasQuestionMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
</Button>
</div>
{canvasQuestions.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<ClipboardList className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Nenhuma pergunta diagnóstica cadastrada</p>
<p className="text-sm">Adicione perguntas para avaliar este bloco</p>
</div>
) : (
<Accordion type="multiple" className="space-y-2">
{canvasQuestions.map((question, idx) => (
<AccordionItem key={question.id} value={`q-${question.id}`} className="border rounded-lg px-3">
<AccordionTrigger className="py-3 hover:no-underline">
<div className="flex items-center gap-3 flex-1 text-left">
<span className="text-muted-foreground text-sm font-normal">{idx + 1}.</span>
<span className="flex-1 text-sm">{question.question}</span>
<Badge variant={question.score !== null ? "default" : "outline"} className="mr-2">
{question.score !== null ? `${question.score}/10` : "—"}
</Badge>
</div>
</AccordionTrigger>
<AccordionContent className="pb-3 space-y-3">
<div className="space-y-2">
<Label className="text-xs">Resposta / Observações</Label>
<Textarea
key={`answer-${question.id}-${question.answer || ''}`}
placeholder="Responda ou faça observações sobre esta pergunta..."
defaultValue={question.answer || ""}
rows={2}
onBlur={(e) => {
if (e.target.value !== (question.answer || "")) {
updateCanvasQuestionMutation.mutate({ id: question.id, answer: e.target.value });
}
}}
/>
</div>
<div className="space-y-2">
<Label className="text-xs">Pontuação (0-10)</Label>
<div className="flex items-center gap-3">
<Slider
value={[question.score ?? 0]}
onValueChange={([value]) => {
updateCanvasQuestionMutation.mutate({ id: question.id, score: value });
}}
max={10}
step={1}
className="flex-1"
/>
<span className="w-8 text-center font-medium">{question.score ?? 0}</span>
</div>
</div>
<div className="flex justify-end">
<Button
type="button"
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
onClick={() => deleteCanvasQuestionMutation.mutate(question.id)}
>
<Trash2 className="h-3.5 w-3.5 mr-1" /> Excluir
</Button>
</div>
</AccordionContent>
</AccordionItem>
))}
</Accordion>
)}
</>
)}
</TabsContent>
<TabsContent value="outputs" className="mt-4 space-y-4">
{!editingBlock?.id ? (
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Salve o bloco primeiro para adicionar saídas esperadas.
</AlertDescription>
</Alert>
) : (
<>
<div className="flex gap-2">
<Input
placeholder="Nova saída esperada..."
value={newOutputText}
onChange={(e) => setNewOutputText(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && newOutputText.trim()) {
e.preventDefault();
addCanvasOutputMutation.mutate({
description: newOutputText.trim(),
orderIndex: canvasOutputs.length
});
}
}}
data-testid="input-new-output"
/>
<Button
type="button"
disabled={!newOutputText.trim() || addCanvasOutputMutation.isPending}
onClick={() => {
if (newOutputText.trim()) {
addCanvasOutputMutation.mutate({
description: newOutputText.trim(),
orderIndex: canvasOutputs.length
});
}
}}
>
{addCanvasOutputMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
</Button>
</div>
{canvasOutputs.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<FileCheck className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>Nenhuma saída esperada cadastrada</p>
<p className="text-sm">Defina os entregáveis para este bloco</p>
</div>
) : (
<div className="space-y-2">
{canvasOutputs.map((output) => (
<div key={output.id} className="flex items-center gap-3 p-3 border rounded-lg hover:bg-muted/50">
<Checkbox
checked={output.completed}
disabled
/>
<span className={`flex-1 text-sm ${output.completed ? 'line-through text-muted-foreground' : ''}`}>
{output.description}
</span>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={() => deleteCanvasOutputMutation.mutate(output.id)}
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
))}
</div>
)}
</>
)}
</TabsContent>
</ScrollArea>
</Tabs>
<DialogFooter className="border-t pt-4 mt-4">
<Button type="button" variant="outline" onClick={() => setEditingBlock(null)}>Fechar</Button>
<Button type="submit" form="canvas-block-form" disabled={upsertCanvasBlockMutation.isPending} data-testid="btn-save-block">
{upsertCanvasBlockMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Salvar Bloco
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</TooltipProvider>
)}
{activeTab === "processes" && (
<div className="space-y-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3">
<div>
<h2 className="text-xl md:text-2xl font-bold">Mapeamento de Processos</h2>
<p className="text-sm md:text-base text-muted-foreground">Mapeie os processos de negócio</p>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<Select value={selectedProjectId?.toString() || ""} onValueChange={(v) => { setSelectedProjectId(v ? parseInt(v) : null); setSelectedProcessId(null); }}>
<SelectTrigger className="w-full md:w-64" data-testid="select-project-processes">
<SelectValue placeholder="Selecione um projeto" />
</SelectTrigger>
<SelectContent>
{projects.map(p => (
<SelectItem key={p.id} value={p.id.toString()}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
{selectedProjectId && (
<Button onClick={() => setShowNewProcessDialog(true)} data-testid="btn-new-process">
<Plus className="h-4 w-4 mr-2" /> Novo Processo
</Button>
)}
</div>
</div>
{!selectedProjectId ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Network className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">Selecione um Projeto</h3>
<p className="text-muted-foreground text-center max-w-md">
Escolha um projeto acima para mapear seus processos.
</p>
</CardContent>
</Card>
) : processes.length === 0 ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Network className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">Nenhum processo mapeado</h3>
<p className="text-muted-foreground text-center max-w-md mb-4">
Comece mapeando os processos de negócio deste cliente.
</p>
<Button onClick={() => setShowNewProcessDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Criar Primeiro Processo
</Button>
</CardContent>
</Card>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="col-span-1 space-y-2">
<h3 className="font-semibold text-sm text-muted-foreground mb-2">PROCESSOS</h3>
{processes.map(process => (
<Card
key={process.id}
className={`cursor-pointer hover:shadow-md transition-shadow ${selectedProcessId === process.id ? 'ring-2 ring-primary' : ''}`}
onClick={() => setSelectedProcessId(process.id)}
data-testid={`process-card-${process.id}`}
>
<CardContent className="p-3">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{process.name}</p>
<p className="text-xs text-muted-foreground">{PROCESS_CATEGORIES.find(c => c.id === process.category)?.label || process.category}</p>
</div>
<Badge variant="outline">{process.status}</Badge>
</div>
</CardContent>
</Card>
))}
</div>
<div className="col-span-2">
{selectedProcessId ? (
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle>{processes.find(p => p.id === selectedProcessId)?.name}</CardTitle>
<CardDescription>{processes.find(p => p.id === selectedProcessId)?.description}</CardDescription>
</div>
<div className="flex items-center border rounded-lg overflow-hidden">
<button
className={`px-3 py-1.5 text-xs font-medium transition-colors ${processView === "steps" ? "bg-primary text-primary-foreground" : "hover:bg-muted"}`}
onClick={() => setProcessView("steps")}
data-testid="btn-view-steps"
>
Etapas
</button>
<button
className={`px-3 py-1.5 text-xs font-medium transition-colors ${processView === "diagram" ? "bg-primary text-primary-foreground" : "hover:bg-muted"}`}
onClick={() => setProcessView("diagram")}
data-testid="btn-view-diagram"
>
Diagrama BPMN
</button>
</div>
</div>
</CardHeader>
<CardContent>
{processView === "steps" ? (
<>
<h4 className="font-semibold mb-3">Etapas do Processo</h4>
{processSteps.length === 0 ? (
<p className="text-muted-foreground text-sm">Nenhuma etapa cadastrada ainda.</p>
) : (
<div className="space-y-2">
{processSteps.sort((a, b) => a.order - b.order).map((step, idx) => (
<div key={step.id} className="flex items-start gap-3 p-3 border rounded-lg">
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary text-primary-foreground font-medium text-sm">
{idx + 1}
</div>
<div className="flex-1">
<p className="font-medium">{step.name}</p>
{step.description && <p className="text-sm text-muted-foreground">{step.description}</p>}
<div className="flex gap-4 mt-2 text-xs text-muted-foreground">
{step.responsible && <span>Responsável: {step.responsible}</span>}
{step.duration && <span>Duração: {step.duration}</span>}
</div>
{step.painPoints && (
<div className="mt-2 p-2 bg-red-50 rounded text-sm text-red-700">
<strong>Ponto de dor:</strong> {step.painPoints}
</div>
)}
</div>
</div>
))}
</div>
)}
</>
) : (
<div className="h-[500px]">
<BpmnDiagram
processName={processes.find(p => p.id === selectedProcessId)?.name}
onSave={(data) => {
console.log("BPMN saved:", data);
}}
/>
</div>
)}
</CardContent>
</Card>
) : (
<Card className="border-dashed h-full">
<CardContent className="flex flex-col items-center justify-center py-16">
<ClipboardList className="h-12 w-12 text-muted-foreground mb-3" />
<p className="text-muted-foreground">Selecione um processo para ver detalhes</p>
</CardContent>
</Card>
)}
</div>
</div>
)}
</div>
)}
{activeTab === "swot" && (
<TooltipProvider>
<div className="space-y-4">
<Alert className="bg-green-50 border-green-200">
<Lightbulb className="h-4 w-4 text-green-600" />
<AlertDescription className="text-green-800">
<strong>Dica:</strong> {MODULE_HELP.swot.tips[0]}
</AlertDescription>
</Alert>
{/* Industry Templates Selector */}
<Card className="bg-gradient-to-r from-purple-50 to-blue-50 border-purple-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<Lightbulb className="h-4 w-4 text-purple-600" />
Templates por Indústria
</CardTitle>
<CardDescription>Selecione um setor para ver sugestões de análise SWOT</CardDescription>
</CardHeader>
<CardContent className="pb-3">
<div className="flex flex-wrap gap-2">
{Object.keys(INDUSTRY_TEMPLATES).map(industry => (
<Button
key={industry}
variant={selectedIndustryTemplate === industry ? "default" : "outline"}
size="sm"
onClick={() => setSelectedIndustryTemplate(selectedIndustryTemplate === industry ? null : industry)}
data-testid={`btn-template-${industry}`}
>
{industry.charAt(0).toUpperCase() + industry.slice(1)}
</Button>
))}
</div>
{selectedIndustryTemplate && INDUSTRY_TEMPLATES[selectedIndustryTemplate] && (
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
<div className="p-2 bg-green-100 rounded">
<strong className="text-green-800">Forças:</strong>
<ul className="mt-1 space-y-1">
{INDUSTRY_TEMPLATES[selectedIndustryTemplate].strengths.slice(0, 2).map((s, i) => (
<li key={i} className="text-green-700"> {s}</li>
))}
</ul>
</div>
<div className="p-2 bg-red-100 rounded">
<strong className="text-red-800">Fraquezas:</strong>
<ul className="mt-1 space-y-1">
{INDUSTRY_TEMPLATES[selectedIndustryTemplate].weaknesses.slice(0, 2).map((s, i) => (
<li key={i} className="text-red-700"> {s}</li>
))}
</ul>
</div>
<div className="p-2 bg-blue-100 rounded">
<strong className="text-blue-800">Oportunidades:</strong>
<ul className="mt-1 space-y-1">
{INDUSTRY_TEMPLATES[selectedIndustryTemplate].opportunities.slice(0, 2).map((s, i) => (
<li key={i} className="text-blue-700"> {s}</li>
))}
</ul>
</div>
<div className="p-2 bg-orange-100 rounded">
<strong className="text-orange-800">Ameaças:</strong>
<ul className="mt-1 space-y-1">
{INDUSTRY_TEMPLATES[selectedIndustryTemplate].threats.slice(0, 2).map((s, i) => (
<li key={i} className="text-orange-700"> {s}</li>
))}
</ul>
</div>
</div>
)}
</CardContent>
</Card>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div>
<h2 className="text-xl md:text-2xl font-bold">Análise SWOT</h2>
<p className="text-sm md:text-base text-muted-foreground">Análises SWOT setoriais</p>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="font-semibold">{MODULE_HELP.swot.title}</p>
<p className="text-sm">{MODULE_HELP.swot.description}</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-2">
<Select value={selectedProjectId?.toString() || ""} onValueChange={(v) => { setSelectedProjectId(v ? parseInt(v) : null); setSelectedSwotId(null); }}>
<SelectTrigger className="w-full md:w-64" data-testid="select-project-swot">
<SelectValue placeholder="Selecione um projeto" />
</SelectTrigger>
<SelectContent>
{projects.map(p => (
<SelectItem key={p.id} value={p.id.toString()}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
{selectedProjectId && (
<Button onClick={() => setShowNewSwotDialog(true)} data-testid="btn-new-swot">
<Plus className="h-4 w-4 mr-2" /> Nova Análise
</Button>
)}
</div>
</div>
{!selectedProjectId ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Layers className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">Selecione um Projeto</h3>
<p className="text-muted-foreground text-center max-w-md">
Escolha um projeto acima para realizar análises SWOT.
</p>
</CardContent>
</Card>
) : swotAnalyses.length === 0 ? (
<Card className="border-dashed">
<CardContent className="flex flex-col items-center justify-center py-16">
<Layers className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">Nenhuma análise SWOT</h3>
<p className="text-muted-foreground text-center max-w-md mb-4">
Crie análises SWOT para diferentes setores do negócio.
</p>
<Button onClick={() => setShowNewSwotDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Criar Primeira Análise
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-4">
<div className="flex gap-2 flex-wrap">
{swotAnalyses.map(analysis => (
<Button
key={analysis.id}
variant={selectedSwotId === analysis.id ? "default" : "outline"}
onClick={() => setSelectedSwotId(analysis.id)}
data-testid={`swot-tab-${analysis.id}`}
>
{analysis.name}
<Badge variant="secondary" className="ml-2">
{SWOT_SECTORS.find(s => s.id === analysis.sector)?.label || analysis.sector}
</Badge>
</Button>
))}
</div>
{selectedSwotId && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="bg-green-50 border-green-200">
<CardHeader className="pb-2">
<CardTitle className="text-green-800 flex items-center gap-2">
<CheckCircle className="h-5 w-5" />
Forças
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{swotItems.filter(i => i.type === "strength").map(item => (
<div
key={item.id}
className="p-2 bg-white rounded border cursor-pointer hover:bg-green-100 transition-colors"
data-testid={`swot-item-${item.id}`}
onClick={() => { setEditingSwotItem(item); setSwotItemEditTab("details"); setSwotPdcaStatus(item.pdcaStatus || "plan"); }}
>
<p className="text-sm font-medium">{item.title || item.description}</p>
{item.title && <p className="text-xs text-muted-foreground line-clamp-2">{item.description}</p>}
<div className="flex gap-1 mt-1 flex-wrap">
<Badge variant="outline" className="text-xs">{item.priorityLevel || "média"}</Badge>
{item.impactScore && <Badge variant="secondary" className="text-xs">Impacto: {item.impactScore}/5</Badge>}
{item.pdcaStatus && item.pdcaStatus !== "plan" && (
<Badge className="text-xs bg-blue-100 text-blue-800">PDCA: {item.pdcaStatus}</Badge>
)}
</div>
</div>
))}
<Button variant="ghost" size="sm" className="w-full" onClick={() => createSwotItemMutation.mutate({ type: "strength", description: "Nova força", impact: "medium" })}>
<Plus className="h-4 w-4 mr-1" /> Adicionar
</Button>
</div>
</CardContent>
</Card>
<Card className="bg-red-50 border-red-200">
<CardHeader className="pb-2">
<CardTitle className="text-red-800 flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
Fraquezas
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{swotItems.filter(i => i.type === "weakness").map(item => (
<div
key={item.id}
className="p-2 bg-white rounded border cursor-pointer hover:bg-red-100 transition-colors"
data-testid={`swot-item-${item.id}`}
onClick={() => { setEditingSwotItem(item); setSwotItemEditTab("details"); setSwotPdcaStatus(item.pdcaStatus || "plan"); }}
>
<p className="text-sm font-medium">{item.title || item.description}</p>
{item.title && <p className="text-xs text-muted-foreground line-clamp-2">{item.description}</p>}
<div className="flex gap-1 mt-1 flex-wrap">
<Badge variant="outline" className="text-xs">{item.priorityLevel || "média"}</Badge>
{item.impactScore && <Badge variant="secondary" className="text-xs">Impacto: {item.impactScore}/5</Badge>}
{item.pdcaStatus && item.pdcaStatus !== "plan" && (
<Badge className="text-xs bg-blue-100 text-blue-800">PDCA: {item.pdcaStatus}</Badge>
)}
</div>
</div>
))}
<Button variant="ghost" size="sm" className="w-full" onClick={() => createSwotItemMutation.mutate({ type: "weakness", description: "Nova fraqueza", impact: "medium" })}>
<Plus className="h-4 w-4 mr-1" /> Adicionar
</Button>
</div>
</CardContent>
</Card>
<Card className="bg-blue-50 border-blue-200">
<CardHeader className="pb-2">
<CardTitle className="text-blue-800 flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Oportunidades
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{swotItems.filter(i => i.type === "opportunity").map(item => (
<div
key={item.id}
className="p-2 bg-white rounded border cursor-pointer hover:bg-blue-100 transition-colors"
data-testid={`swot-item-${item.id}`}
onClick={() => { setEditingSwotItem(item); setSwotItemEditTab("details"); setSwotPdcaStatus(item.pdcaStatus || "plan"); }}
>
<p className="text-sm font-medium">{item.title || item.description}</p>
{item.title && <p className="text-xs text-muted-foreground line-clamp-2">{item.description}</p>}
<div className="flex gap-1 mt-1 flex-wrap">
<Badge variant="outline" className="text-xs">{item.priorityLevel || "média"}</Badge>
{item.impactScore && <Badge variant="secondary" className="text-xs">Impacto: {item.impactScore}/5</Badge>}
{item.pdcaStatus && item.pdcaStatus !== "plan" && (
<Badge className="text-xs bg-blue-100 text-blue-800">PDCA: {item.pdcaStatus}</Badge>
)}
</div>
</div>
))}
<Button variant="ghost" size="sm" className="w-full" onClick={() => createSwotItemMutation.mutate({ type: "opportunity", description: "Nova oportunidade", impact: "medium" })}>
<Plus className="h-4 w-4 mr-1" /> Adicionar
</Button>
</div>
</CardContent>
</Card>
<Card className="bg-yellow-50 border-yellow-200">
<CardHeader className="pb-2">
<CardTitle className="text-yellow-800 flex items-center gap-2">
<AlertCircle className="h-5 w-5" />
Ameaças
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{swotItems.filter(i => i.type === "threat").map(item => (
<div
key={item.id}
className="p-2 bg-white rounded border cursor-pointer hover:bg-yellow-100 transition-colors"
data-testid={`swot-item-${item.id}`}
onClick={() => { setEditingSwotItem(item); setSwotItemEditTab("details"); setSwotPdcaStatus(item.pdcaStatus || "plan"); }}
>
<p className="text-sm font-medium">{item.title || item.description}</p>
{item.title && <p className="text-xs text-muted-foreground line-clamp-2">{item.description}</p>}
<div className="flex gap-1 mt-1 flex-wrap">
<Badge variant="outline" className="text-xs">{item.priorityLevel || "média"}</Badge>
{item.impactScore && <Badge variant="secondary" className="text-xs">Impacto: {item.impactScore}/5</Badge>}
{item.pdcaStatus && item.pdcaStatus !== "plan" && (
<Badge className="text-xs bg-blue-100 text-blue-800">PDCA: {item.pdcaStatus}</Badge>
)}
</div>
</div>
))}
<Button variant="ghost" size="sm" className="w-full" onClick={() => createSwotItemMutation.mutate({ type: "threat", description: "Nova ameaça", impact: "medium" })}>
<Plus className="h-4 w-4 mr-1" /> Adicionar
</Button>
</div>
</CardContent>
</Card>
</div>
)}
</div>
)}
<Dialog open={showNewSwotDialog} onOpenChange={(open) => { setShowNewSwotDialog(open); if (!open) setNewSwotSector(""); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Nova Análise SWOT</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createSwotAnalysisMutation.mutate({
name: formData.get("name") as string,
description: formData.get("description") as string,
sector: newSwotSector || undefined,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="swot-name">Nome da Análise</Label>
<Input id="swot-name" name="name" required placeholder="Ex: Análise Comercial Q1" data-testid="input-swot-name" />
</div>
<div className="space-y-2">
<Label htmlFor="swot-description">Descrição</Label>
<Textarea id="swot-description" name="description" placeholder="Descreva o contexto desta análise..." data-testid="input-swot-description" />
</div>
<div className="space-y-2">
<Label htmlFor="swot-sector">Setor</Label>
<Select value={newSwotSector} onValueChange={setNewSwotSector}>
<SelectTrigger data-testid="select-swot-sector">
<SelectValue placeholder="Selecione um setor" />
</SelectTrigger>
<SelectContent>
{SWOT_SECTORS.map(sector => (
<SelectItem key={sector.id} value={sector.id}>{sector.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowNewSwotDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={createSwotAnalysisMutation.isPending} data-testid="btn-submit-swot">
{createSwotAnalysisMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={!!editingSwotItem} onOpenChange={(open) => { if (!open) setEditingSwotItem(null); }}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Edit className="h-5 w-5" />
Editar Item SWOT
{editingSwotItem && (
<Badge variant="outline" className="ml-2">
{editingSwotItem.type === "strength" ? "Força" :
editingSwotItem.type === "weakness" ? "Fraqueza" :
editingSwotItem.type === "opportunity" ? "Oportunidade" : "Ameaça"}
</Badge>
)}
</DialogTitle>
</DialogHeader>
{editingSwotItem && (
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const impactScoreVal = parseInt(formData.get("impactScore") as string) || 3;
const impactMap: Record<number, string> = { 1: "low", 2: "low", 3: "medium", 4: "high", 5: "high" };
updateSwotItemMutation.mutate({
id: editingSwotItem.id,
data: {
title: formData.get("title") as string || null,
description: formData.get("description") as string,
impact: impactMap[impactScoreVal] || "medium",
priorityLevel: formData.get("priorityLevel") as string,
impactScore: impactScoreVal,
actionPlan: formData.get("actionPlan") as string || null,
result: formData.get("result") as string || null,
pdcaStatus: formData.get("pdcaStatus") as string,
responsible: formData.get("responsible") as string || null,
},
});
}}>
<Tabs value={swotItemEditTab} onValueChange={setSwotItemEditTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="details" data-testid="tab-swot-details">
<FileText className="h-4 w-4 mr-2" />
Detalhes
</TabsTrigger>
<TabsTrigger value="pdca" data-testid="tab-swot-pdca">
<RefreshCw className="h-4 w-4 mr-2" />
Ciclo PDCA
</TabsTrigger>
</TabsList>
<TabsContent value="details" className="space-y-4">
<div className="space-y-2">
<Label htmlFor="swot-item-title">Título</Label>
<Input
id="swot-item-title"
name="title"
defaultValue={editingSwotItem.title || ""}
placeholder="Título curto do item"
data-testid="input-swot-item-title"
/>
</div>
<div className="space-y-2">
<Label htmlFor="swot-item-description">Descrição *</Label>
<Textarea
id="swot-item-description"
name="description"
defaultValue={editingSwotItem.description}
required
placeholder="Descreva detalhadamente este item..."
rows={4}
data-testid="input-swot-item-description"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="swot-item-priority">Prioridade</Label>
<Select name="priorityLevel" defaultValue={editingSwotItem.priorityLevel || "medium"}>
<SelectTrigger data-testid="select-swot-item-priority">
<SelectValue placeholder="Selecione a prioridade" />
</SelectTrigger>
<SelectContent>
<SelectItem value="baixa">Baixa</SelectItem>
<SelectItem value="medium">Média</SelectItem>
<SelectItem value="alta">Alta</SelectItem>
<SelectItem value="critica">Crítica</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="swot-item-impact">Impacto (1-5)</Label>
<Select name="impactScore" defaultValue={String(editingSwotItem.impactScore || 3)}>
<SelectTrigger data-testid="select-swot-item-impact">
<SelectValue placeholder="Nível de impacto" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">1 - Muito Baixo</SelectItem>
<SelectItem value="2">2 - Baixo</SelectItem>
<SelectItem value="3">3 - Moderado</SelectItem>
<SelectItem value="4">4 - Alto</SelectItem>
<SelectItem value="5">5 - Muito Alto</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="swot-item-responsible">Responsável</Label>
<Input
id="swot-item-responsible"
name="responsible"
defaultValue={editingSwotItem.responsible || ""}
placeholder="Quem é responsável por este item?"
data-testid="input-swot-item-responsible"
/>
</div>
</TabsContent>
<TabsContent value="pdca" className="space-y-4">
<div className="space-y-2">
<Label>Status do Ciclo PDCA</Label>
<input type="hidden" name="pdcaStatus" value={swotPdcaStatus} />
<div className="flex flex-wrap gap-2">
<Button
type="button"
size="sm"
variant={swotPdcaStatus === "plan" ? "default" : "outline"}
className={swotPdcaStatus === "plan" ? "bg-blue-600 hover:bg-blue-700" : ""}
onClick={() => setSwotPdcaStatus("plan")}
data-testid="btn-pdca-plan"
>
<FileText className="h-4 w-4 mr-1" />
Planejar
</Button>
<Button
type="button"
size="sm"
variant={swotPdcaStatus === "do" ? "default" : "outline"}
className={swotPdcaStatus === "do" ? "bg-orange-500 hover:bg-orange-600" : "text-orange-600 border-orange-300"}
onClick={() => setSwotPdcaStatus("do")}
data-testid="btn-pdca-do"
>
<CirclePlay className="h-4 w-4 mr-1" />
Executar
</Button>
<Button
type="button"
size="sm"
variant={swotPdcaStatus === "check" ? "default" : "outline"}
className={swotPdcaStatus === "check" ? "bg-purple-600 hover:bg-purple-700" : "text-purple-600 border-purple-300"}
onClick={() => setSwotPdcaStatus("check")}
data-testid="btn-pdca-check"
>
<CheckCircle className="h-4 w-4 mr-1" />
Verificar
</Button>
<Button
type="button"
size="sm"
variant={swotPdcaStatus === "act" ? "default" : "outline"}
className={swotPdcaStatus === "act" ? "bg-amber-500 hover:bg-amber-600" : "text-amber-600 border-amber-300"}
onClick={() => setSwotPdcaStatus("act")}
data-testid="btn-pdca-act"
>
<RefreshCw className="h-4 w-4 mr-1" />
Agir
</Button>
<Button
type="button"
size="sm"
variant={swotPdcaStatus === "done" ? "default" : "outline"}
className={swotPdcaStatus === "done" ? "bg-green-600 hover:bg-green-700" : "text-green-600 border-green-300"}
onClick={() => setSwotPdcaStatus("done")}
data-testid="btn-pdca-done"
>
<CheckCircle className="h-4 w-4 mr-1" />
Concluído
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="swot-item-action-plan">Plano de Ação</Label>
<Textarea
id="swot-item-action-plan"
name="actionPlan"
defaultValue={editingSwotItem.actionPlan || ""}
placeholder="Descreva as ações necessárias para tratar este item..."
rows={4}
data-testid="input-swot-item-action-plan"
/>
</div>
<div className="space-y-2">
<Label htmlFor="swot-item-result">Resultado / Verificação</Label>
<Textarea
id="swot-item-result"
name="result"
defaultValue={editingSwotItem.result || ""}
placeholder="Documente os resultados obtidos e lições aprendidas..."
rows={4}
data-testid="input-swot-item-result"
/>
</div>
</TabsContent>
</Tabs>
<DialogFooter className="mt-6 flex justify-between">
<Button
type="button"
variant="destructive"
disabled={deleteSwotItemMutation.isPending}
onClick={() => {
if (confirm("Tem certeza que deseja excluir este item?")) {
deleteSwotItemMutation.mutate(editingSwotItem.id, {
onSuccess: () => setEditingSwotItem(null),
});
}
}}
data-testid="btn-delete-swot-item"
>
<Trash2 className="h-4 w-4 mr-2" />
Excluir
</Button>
<div className="flex gap-2">
<Button type="button" variant="outline" onClick={() => setEditingSwotItem(null)}>
Cancelar
</Button>
<Button type="submit" disabled={updateSwotItemMutation.isPending} data-testid="btn-save-swot-item">
{updateSwotItemMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Salvar
</Button>
</div>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
</div>
</TooltipProvider>
)}
{activeTab === "pdca" && (
<TooltipProvider>
<div className="space-y-4">
<Alert className="bg-amber-50 border-amber-200">
<Lightbulb className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-amber-800">
<strong>Dica:</strong> {MODULE_HELP.pdca.tips[0]}
</AlertDescription>
</Alert>
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-2">
<div>
<h2 className="text-2xl font-bold">PDCA</h2>
<p className="text-muted-foreground">Gerencie ciclos de melhoria contínua</p>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</Button>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="font-semibold">{MODULE_HELP.pdca.title}</p>
<p className="text-sm">{MODULE_HELP.pdca.description}</p>
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Select value={selectedProjectId?.toString() || "all"} onValueChange={(v) => setSelectedProjectId(v === "all" ? null : parseInt(v))}>
<SelectTrigger className="w-full md:w-[200px]" data-testid="select-pdca-project">
<SelectValue placeholder="Todos os projetos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos os projetos</SelectItem>
{projects.map(p => (
<SelectItem key={p.id} value={p.id.toString()}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => setShowNewPdcaDialog(true)} data-testid="btn-new-pdca">
<Plus className="h-4 w-4 mr-2" /> Novo Ciclo
</Button>
</div>
</div>
{!selectedProjectId ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<RefreshCw className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">Selecione um Projeto</h3>
<p className="text-muted-foreground text-center max-w-md">
Escolha um projeto acima para visualizar e gerenciar itens do ciclo PDCA.
</p>
</CardContent>
</Card>
) : (
<div className="space-y-4">
{(() => {
const allItems = pdcaOverview?.items || [];
const sourceFilteredItems = allItems.filter(item => pdcaSourceFilter === "all" || item.source === pdcaSourceFilter);
const phaseFilteredItems = allItems.filter(item => pdcaPhaseFilter === "all" || item.pdcaStatus?.toLowerCase() === pdcaPhaseFilter);
const countByPhase = (phase: string) => sourceFilteredItems.filter(item => item.pdcaStatus?.toLowerCase() === phase).length;
const countBySource = (source: string) => phaseFilteredItems.filter(item => item.source === source).length;
return (
<>
{/* Phase filter tabs */}
<div className="flex gap-2 flex-wrap">
{[
{ id: "all", label: "Todos", count: sourceFilteredItems.length },
{ id: "plan", label: "Plan", count: countByPhase("plan") },
{ id: "do", label: "Do", count: countByPhase("do") },
{ id: "check", label: "Check", count: countByPhase("check") },
{ id: "act", label: "Act", count: countByPhase("act") },
{ id: "done", label: "Done", count: countByPhase("done") },
].map(phase => (
<Button
key={phase.id}
variant={pdcaPhaseFilter === phase.id ? "default" : "outline"}
size="sm"
onClick={() => setPdcaPhaseFilter(phase.id)}
data-testid={`pdca-phase-${phase.id}`}
>
{phase.label} ({phase.count})
</Button>
))}
</div>
{/* Source filter tabs */}
<div className="flex gap-2 flex-wrap">
{[
{ id: "all", label: "Todos", icon: Layers, count: phaseFilteredItems.length },
{ id: "canvas", label: "Canvas", icon: Target, count: countBySource("canvas") },
{ id: "processes", label: "Processos", icon: Network, count: countBySource("processes") },
{ id: "swot", label: "SWOT", icon: TrendingUp, count: countBySource("swot") },
{ id: "requirements", label: "Requisitos", icon: FileCheck, count: countBySource("requirements") },
].map(source => (
<Button
key={source.id}
variant={pdcaSourceFilter === source.id ? "secondary" : "ghost"}
size="sm"
onClick={() => setPdcaSourceFilter(source.id)}
data-testid={`pdca-source-${source.id}`}
>
<source.icon className="h-4 w-4 mr-1" />
{source.label} ({source.count})
</Button>
))}
</div>
</>
);
})()}
{/* Progress bars by source */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ id: "canvas", label: "Canvas", stats: pdcaOverview?.stats?.canvas },
{ id: "processes", label: "Processos", stats: pdcaOverview?.stats?.processes },
{ id: "swot", label: "SWOT", stats: pdcaOverview?.stats?.swot },
{ id: "requirements", label: "Requisitos", stats: pdcaOverview?.stats?.requirements },
].map(source => {
const total = source.stats?.total || 0;
const done = source.stats?.done || 0;
const percent = total > 0 ? Math.round((done / total) * 100) : 0;
return (
<Card key={source.id} className="p-3">
<p className="text-sm font-medium">{source.label}</p>
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{done}/{total} concluídos</span>
<span>{percent}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2 mt-1">
<div className="bg-green-500 h-2 rounded-full transition-all" style={{ width: `${percent}%` }} />
</div>
</Card>
);
})}
</div>
{/* Items list */}
<div className="space-y-2">
{(() => {
const filteredItems = (pdcaOverview?.items || [])
.filter(item => pdcaPhaseFilter === "all" || item.pdcaStatus?.toLowerCase() === pdcaPhaseFilter)
.filter(item => pdcaSourceFilter === "all" || item.source === pdcaSourceFilter);
if (filteredItems.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-8">
<p className="text-muted-foreground">
{(pdcaOverview?.items || []).length === 0
? "Nenhum item PDCA encontrado para este projeto."
: "Nenhum item encontrado para os filtros selecionados."}
</p>
<p className="text-sm text-muted-foreground mt-2">
{(pdcaOverview?.items || []).length === 0
? "Adicione itens no Canvas, Processos, SWOT ou Requisitos para gerenciá-los aqui."
: "Tente alterar os filtros de fase ou origem."}
</p>
</CardContent>
</Card>
);
}
return filteredItems.map(item => {
const status = item.pdcaStatus?.toLowerCase() || "plan";
return (
<Card key={`${item.source}-${item.id}`} className="p-4" data-testid={`pdca-item-${item.source}-${item.id}`}>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<Badge variant="outline" className="text-xs">
{item.source === "canvas" ? "Canvas" :
item.source === "processes" ? "Processo" :
item.source === "swot" ? "SWOT" : "Requisito"}
</Badge>
<Badge className={
status === "plan" ? "bg-blue-100 text-blue-800" :
status === "do" ? "bg-green-100 text-green-800" :
status === "check" ? "bg-yellow-100 text-yellow-800" :
status === "act" ? "bg-purple-100 text-purple-800" :
status === "done" ? "bg-gray-200 text-gray-800" :
"bg-gray-100 text-gray-800"
}>
{status.toUpperCase()}
</Badge>
</div>
<h4 className="font-medium">{item.title}</h4>
{item.description && (
<p className="text-sm text-muted-foreground line-clamp-2">{item.description}</p>
)}
{item.pdcaActionPlan && (
<div className="mt-2 p-2 bg-blue-50 rounded text-sm">
<strong>Plano:</strong> {item.pdcaActionPlan}
</div>
)}
</div>
</div>
</Card>
);
});
})()}
</div>
</div>
)}
<Dialog open={showNewPdcaDialog} onOpenChange={setShowNewPdcaDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Ciclo PDCA</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createPdcaCycleMutation.mutate({
title: formData.get("title") as string,
description: formData.get("description") as string || undefined,
projectId: selectedProjectId || undefined,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="pdca-title">Título</Label>
<Input id="pdca-title" name="title" required placeholder="Ex: Otimização do processo de vendas" data-testid="input-pdca-title" />
</div>
<div className="space-y-2">
<Label htmlFor="pdca-description">Descrição</Label>
<Textarea id="pdca-description" name="description" placeholder="Descreva o objetivo deste ciclo..." data-testid="input-pdca-description" />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowNewPdcaDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={createPdcaCycleMutation.isPending} data-testid="btn-submit-pdca">
{createPdcaCycleMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
</TooltipProvider>
)}
{activeTab === "requirements" && (
<div className="space-y-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h2 className="text-2xl font-bold">Requisitos</h2>
<p className="text-muted-foreground">Gerencie requisitos de projetos</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<Select value={selectedProjectId?.toString() || "all"} onValueChange={(v) => setSelectedProjectId(v === "all" ? null : parseInt(v))}>
<SelectTrigger className="w-full md:w-[200px]" data-testid="select-req-project">
<SelectValue placeholder="Todos os projetos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos os projetos</SelectItem>
{projects.map(p => (
<SelectItem key={p.id} value={p.id.toString()}>{p.name}</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={() => setShowNewRequirementDialog(true)} data-testid="btn-new-requirement">
<Plus className="h-4 w-4 mr-2" /> Novo Requisito
</Button>
</div>
</div>
{requirements.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<FileCheck className="h-16 w-16 text-muted-foreground mb-4" />
<h3 className="text-xl font-semibold mb-2">Nenhum requisito</h3>
<p className="text-muted-foreground text-center max-w-md mb-4">
Crie requisitos para documentar as necessidades do projeto.
</p>
<Button onClick={() => setShowNewRequirementDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Criar Primeiro Requisito
</Button>
</CardContent>
</Card>
) : (
<div className="space-y-2">
{requirements.map(req => (
<Card key={req.id} data-testid={`requirement-${req.id}`}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-2">
{req.code && <Badge variant="outline">{req.code}</Badge>}
<CardTitle className="text-lg">{req.title}</CardTitle>
</div>
<div className="flex gap-2">
<Badge className={req.priority === 'critical' ? 'bg-red-100 text-red-800' : req.priority === 'high' ? 'bg-orange-100 text-orange-800' : 'bg-gray-100 text-gray-800'}>
{req.priority}
</Badge>
<Badge variant="secondary">
{REQUIREMENT_TYPES.find(t => t.id === req.type)?.label || req.type}
</Badge>
<Badge variant="outline">
{REQUIREMENT_STATUS.find(s => s.id === req.status)?.label || req.status}
</Badge>
</div>
</div>
{req.description && <CardDescription>{req.description}</CardDescription>}
</CardHeader>
</Card>
))}
</div>
)}
<Dialog open={showNewRequirementDialog} onOpenChange={setShowNewRequirementDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Requisito</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
if (!newReqTitle.trim()) return;
createRequirementMutation.mutate({
title: newReqTitle,
description: newReqDescription || undefined,
type: newReqType,
priority: newReqPriority,
projectId: selectedProjectId || undefined,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="req-title">Título</Label>
<Input
id="req-title"
value={newReqTitle}
onChange={(e) => setNewReqTitle(e.target.value)}
required
placeholder="Ex: Sistema deve permitir login com email"
data-testid="input-req-title"
/>
</div>
<div className="space-y-2">
<Label htmlFor="req-description">Descrição</Label>
<Textarea
id="req-description"
value={newReqDescription}
onChange={(e) => setNewReqDescription(e.target.value)}
placeholder="Descreva o requisito em detalhes..."
data-testid="input-req-description"
/>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="req-type">Tipo</Label>
<Select value={newReqType} onValueChange={setNewReqType}>
<SelectTrigger data-testid="select-req-type">
<SelectValue />
</SelectTrigger>
<SelectContent>
{REQUIREMENT_TYPES.map(t => (
<SelectItem key={t.id} value={t.id}>{t.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="req-priority">Prioridade</Label>
<Select value={newReqPriority} onValueChange={setNewReqPriority}>
<SelectTrigger data-testid="select-req-priority">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Baixa</SelectItem>
<SelectItem value="medium">Média</SelectItem>
<SelectItem value="high">Alta</SelectItem>
<SelectItem value="critical">Crítica</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowNewRequirementDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={createRequirementMutation.isPending || !newReqTitle.trim()} data-testid="btn-submit-requirement">
{createRequirementMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
)}
{activeTab === "settings" && (
<Card>
<CardHeader>
<CardTitle>Configurações</CardTitle>
<CardDescription>Gerencie sua organização e preferências</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between p-4 border rounded-lg">
<div>
<p className="font-medium">Organização Atual</p>
<p className="text-sm text-muted-foreground">
{tenants[0]?.name || "Nenhuma organização"}
</p>
</div>
<Button variant="outline" onClick={() => setShowNewTenantDialog(true)}>
<Plus className="h-4 w-4 mr-2" /> Nova Organização
</Button>
</div>
</CardContent>
</Card>
)}
{activeTab === "valuation" && (
<ValuationSection tenantId={tenants[0]?.id} />
)}
</div>
</div>
</div>
<Dialog open={showNewTenantDialog} onOpenChange={setShowNewTenantDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Nova Organização</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createTenantMutation.mutate({
name: formData.get("name") as string,
slug: formData.get("slug") as string || undefined,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="tenant-name">Nome da Organização</Label>
<Input id="tenant-name" name="name" placeholder="Ex: Minha Consultoria" required data-testid="input-tenant-name" />
</div>
<div className="space-y-2">
<Label htmlFor="tenant-slug">Identificador (opcional)</Label>
<Input id="tenant-slug" name="slug" placeholder="minha-consultoria" data-testid="input-tenant-slug" />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowNewTenantDialog(false)}>
Cancelar
</Button>
<Button type="submit" disabled={createTenantMutation.isPending} data-testid="btn-submit-tenant">
{createTenantMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showNewClientDialog} onOpenChange={setShowNewClientDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Cadastrar Cliente</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const partnerValue = formData.get("partnerId");
createClientMutation.mutate({
name: formData.get("name"),
tradeName: formData.get("tradeName") || undefined,
cnpj: formData.get("cnpj") || undefined,
email: formData.get("email") || undefined,
phone: formData.get("phone") || undefined,
segment: formData.get("segment") || undefined,
primaryContactName: formData.get("contactName") || undefined,
primaryContactEmail: formData.get("contactEmail") || undefined,
primaryContactPhone: formData.get("contactPhone") || undefined,
partnerId: partnerValue && partnerValue !== "none" ? Number(partnerValue) : undefined,
source: partnerValue && partnerValue !== "none" ? "partner" : "direct",
status: "active"
});
}} className="space-y-4 max-h-[70vh] overflow-y-auto pr-2">
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<Label htmlFor="clientName">Razão Social *</Label>
<Input id="clientName" name="name" required data-testid="input-client-name" />
</div>
<div>
<Label htmlFor="tradeName">Nome Fantasia</Label>
<Input id="tradeName" name="tradeName" data-testid="input-client-tradename" />
</div>
<div>
<Label htmlFor="cnpj">CNPJ</Label>
<Input id="cnpj" name="cnpj" data-testid="input-client-cnpj" />
</div>
<div>
<Label htmlFor="clientEmail">Email</Label>
<Input id="clientEmail" name="email" type="email" data-testid="input-client-email" />
</div>
<div>
<Label htmlFor="clientPhone">Telefone</Label>
<Input id="clientPhone" name="phone" data-testid="input-client-phone" />
</div>
<div>
<Label htmlFor="segment">Segmento</Label>
<Select name="segment">
<SelectTrigger data-testid="select-client-segment">
<SelectValue placeholder="Selecione um segmento" />
</SelectTrigger>
<SelectContent>
<SelectItem value="technology">Tecnologia</SelectItem>
<SelectItem value="retail">Varejo</SelectItem>
<SelectItem value="industry">Indústria</SelectItem>
<SelectItem value="services">Serviços</SelectItem>
<SelectItem value="healthcare">Saúde</SelectItem>
<SelectItem value="education">Educação</SelectItem>
<SelectItem value="finance">Financeiro</SelectItem>
<SelectItem value="other">Outro</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="partnerId">Parceiro Associado</Label>
<Select name="partnerId">
<SelectTrigger data-testid="select-client-partner">
<SelectValue placeholder="Sem parceiro" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Sem parceiro</SelectItem>
{crmPartners.map((partner: any) => (
<SelectItem key={partner.id} value={String(partner.id)}>
{partner.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="border-t pt-4">
<p className="font-medium mb-3">Contato Principal</p>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<Label htmlFor="contactName">Nome do Contato</Label>
<Input id="contactName" name="contactName" data-testid="input-client-contact-name" />
</div>
<div>
<Label htmlFor="contactEmail">Email do Contato</Label>
<Input id="contactEmail" name="contactEmail" type="email" data-testid="input-client-contact-email" />
</div>
<div>
<Label htmlFor="contactPhone">Telefone do Contato</Label>
<Input id="contactPhone" name="contactPhone" data-testid="input-client-contact-phone" />
</div>
</div>
</div>
<Button type="submit" className="w-full" disabled={createClientMutation.isPending} data-testid="btn-submit-client">
{createClientMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Cadastrar
</Button>
</form>
</DialogContent>
</Dialog>
<Dialog open={showNewProjectDialog} onOpenChange={setShowNewProjectDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Projeto</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const projectType = formData.get("projectType") as string;
createProjectMutation.mutate({
name: formData.get("name") as string,
clientId: parseInt(formData.get("clientId") as string),
description: formData.get("description") as string || undefined,
status: formData.get("status") as string || "backlog",
projectType: projectType,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="project-type">Tipo de Projeto</Label>
<Select name="projectType" defaultValue="consultoria">
<SelectTrigger data-testid="select-project-type">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="consultoria">Consultoria</SelectItem>
<SelectItem value="valuation">Valuation</SelectItem>
<SelectItem value="programacao">Programação</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="project-name">Nome do Projeto</Label>
<Input id="project-name" name="name" placeholder="Ex: Diagnóstico Empresarial" required data-testid="input-project-name" />
</div>
<div className="space-y-2">
<Label htmlFor="project-client">Cliente</Label>
{clients.length === 0 ? (
<div className="rounded-md border border-yellow-200 bg-yellow-50 p-3 text-sm text-yellow-800">
Nenhum cliente cadastrado.{" "}
<button
type="button"
className="underline font-medium"
onClick={() => { setShowNewProjectDialog(false); setShowNewClientDialog(true); }}
>
Criar cliente
</button>{" "}
antes de criar um projeto.
</div>
) : (
<Select name="clientId" required>
<SelectTrigger data-testid="select-project-client">
<SelectValue placeholder="Selecione um cliente" />
</SelectTrigger>
<SelectContent>
{clients.map(client => (
<SelectItem key={client.id} value={client.id.toString()}>
{client.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
<div className="space-y-2">
<Label htmlFor="project-description">Descrição</Label>
<Textarea id="project-description" name="description" placeholder="Descreva o escopo do projeto..." rows={3} data-testid="input-project-description" />
</div>
<div className="space-y-2">
<Label htmlFor="project-status">Status Inicial</Label>
<Select name="status" defaultValue="backlog">
<SelectTrigger data-testid="select-project-status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="diagnostico">Diagnóstico</SelectItem>
<SelectItem value="andamento">Em Andamento</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowNewProjectDialog(false)}>
Cancelar
</Button>
<Button type="submit" disabled={createProjectMutation.isPending} data-testid="btn-submit-project">
{createProjectMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Edit Project Dialog */}
<Dialog open={showEditProjectDialog} onOpenChange={(open) => {
setShowEditProjectDialog(open);
if (!open) setEditingProject(null);
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Editar Projeto</DialogTitle>
</DialogHeader>
{editingProject && (
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
updateProjectMutation.mutate({
id: editingProject.id,
name: formData.get("name") as string,
description: formData.get("description") as string || undefined,
status: formData.get("status") as string,
}, {
onSuccess: () => {
setShowEditProjectDialog(false);
setEditingProject(null);
}
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="edit-project-name">Nome do Projeto</Label>
<Input
id="edit-project-name"
name="name"
defaultValue={editingProject.name}
required
data-testid="input-edit-project-name"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-project-description">Descrição</Label>
<Textarea
id="edit-project-description"
name="description"
defaultValue={editingProject.description || ""}
rows={3}
data-testid="input-edit-project-description"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-project-status">Status</Label>
<Select name="status" defaultValue={editingProject.status}>
<SelectTrigger data-testid="select-edit-project-status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="backlog">Backlog</SelectItem>
<SelectItem value="diagnostico">Diagnóstico</SelectItem>
<SelectItem value="andamento">Em Andamento</SelectItem>
<SelectItem value="draft">Rascunho</SelectItem>
<SelectItem value="concluido">Concluído</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => {
setShowEditProjectDialog(false);
setEditingProject(null);
}}>
Cancelar
</Button>
<Button type="submit" disabled={updateProjectMutation.isPending} data-testid="btn-update-project">
{updateProjectMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Salvar
</Button>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
{/* Delete Project Confirmation Dialog */}
<Dialog open={showDeleteProjectConfirm} onOpenChange={(open) => {
setShowDeleteProjectConfirm(open);
if (!open) setProjectToDelete(null);
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Excluir Projeto</DialogTitle>
</DialogHeader>
<div className="py-4">
<p className="text-muted-foreground">
Tem certeza que deseja excluir o projeto <strong>{projectToDelete?.name}</strong>?
</p>
<p className="text-sm text-destructive mt-2">
Esta ação não pode ser desfeita. Todos os dados relacionados ao projeto serão removidos.
</p>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => {
setShowDeleteProjectConfirm(false);
setProjectToDelete(null);
}}>
Cancelar
</Button>
<Button
variant="destructive"
disabled={deleteProjectMutation.isPending}
onClick={() => {
if (projectToDelete) {
deleteProjectMutation.mutate(projectToDelete.id, {
onSuccess: () => {
setShowDeleteProjectConfirm(false);
setProjectToDelete(null);
}
});
}
}}
data-testid="btn-confirm-delete-project"
>
{deleteProjectMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Excluir
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={showNewLeadDialog} onOpenChange={setShowNewLeadDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Lead</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createLeadMutation.mutate({
name: formData.get("name") as string,
email: formData.get("email") as string || undefined,
phone: formData.get("phone") as string || undefined,
company: formData.get("company") as string || undefined,
source: formData.get("source") as string || undefined,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="lead-name">Nome</Label>
<Input id="lead-name" name="name" placeholder="Nome do contato" required data-testid="input-lead-name" />
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="lead-email">Email</Label>
<Input id="lead-email" name="email" type="email" placeholder="email@exemplo.com" data-testid="input-lead-email" />
</div>
<div className="space-y-2">
<Label htmlFor="lead-phone">Telefone</Label>
<Input id="lead-phone" name="phone" placeholder="(11) 99999-9999" data-testid="input-lead-phone" />
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="lead-company">Empresa</Label>
<Input id="lead-company" name="company" placeholder="Nome da empresa" data-testid="input-lead-company" />
</div>
<div className="space-y-2">
<Label htmlFor="lead-source">Origem</Label>
<Select name="source">
<SelectTrigger data-testid="select-lead-source">
<SelectValue placeholder="Selecione..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="website">Website</SelectItem>
<SelectItem value="referral">Indicação</SelectItem>
<SelectItem value="cold_call">Cold Call</SelectItem>
<SelectItem value="event">Evento</SelectItem>
<SelectItem value="social">Redes Sociais</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowNewLeadDialog(false)}>
Cancelar
</Button>
<Button type="submit" disabled={createLeadMutation.isPending} data-testid="btn-submit-lead">
{createLeadMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Global Dialogs for Project Details */}
<Dialog open={showTeamMemberDialog} onOpenChange={(open) => {
setShowTeamMemberDialog(open);
if (!open) setNewMemberRole("");
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Adicionar Membro à Equipe</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
if (!newMemberRole) {
toast.error("Selecione uma função");
return;
}
addTeamMemberMutation.mutate({
name: formData.get("name") as string,
email: formData.get("email") as string || undefined,
role: newMemberRole,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="member-name">Nome</Label>
<Input id="member-name" name="name" required placeholder="Nome do membro" data-testid="input-member-name" />
</div>
<div className="space-y-2">
<Label htmlFor="member-email">Email (opcional)</Label>
<Input id="member-email" name="email" type="email" placeholder="email@exemplo.com" data-testid="input-member-email" />
</div>
<div className="space-y-2">
<Label htmlFor="member-role">Função</Label>
<Select value={newMemberRole} onValueChange={setNewMemberRole}>
<SelectTrigger data-testid="select-member-role">
<SelectValue placeholder="Selecione uma função" />
</SelectTrigger>
<SelectContent>
<SelectItem value="gerente">Gerente de Projeto</SelectItem>
<SelectItem value="analista">Analista</SelectItem>
<SelectItem value="desenvolvedor">Desenvolvedor</SelectItem>
<SelectItem value="designer">Designer</SelectItem>
<SelectItem value="consultor">Consultor</SelectItem>
<SelectItem value="stakeholder">Stakeholder</SelectItem>
<SelectItem value="outro">Outro</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowTeamMemberDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={addTeamMemberMutation.isPending || !newMemberRole} data-testid="btn-submit-member">
{addTeamMemberMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Adicionar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showNewTaskDialog} onOpenChange={(open) => {
setShowNewTaskDialog(open);
if (!open) setNewTaskPriority("medium");
}}>
<DialogContent>
<DialogHeader>
<DialogTitle>Nova Tarefa</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
addProjectTaskMutation.mutate({
title: formData.get("title") as string,
description: formData.get("description") as string || undefined,
priority: newTaskPriority,
assignedTo: formData.get("assignedTo") as string || undefined,
dueDate: formData.get("dueDate") as string || undefined,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="task-title">Título</Label>
<Input id="task-title" name="title" required placeholder="Título da tarefa" data-testid="input-task-title" />
</div>
<div className="space-y-2">
<Label htmlFor="task-description">Descrição (opcional)</Label>
<Textarea id="task-description" name="description" placeholder="Descreva a tarefa..." data-testid="input-task-description" />
</div>
<div className="space-y-2">
<Label htmlFor="task-priority">Prioridade</Label>
<Select value={newTaskPriority} onValueChange={setNewTaskPriority}>
<SelectTrigger data-testid="select-task-priority">
<SelectValue placeholder="Selecione a prioridade" />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Baixa</SelectItem>
<SelectItem value="medium">Média</SelectItem>
<SelectItem value="high">Alta</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="task-assignedTo">Responsável (opcional)</Label>
<Input id="task-assignedTo" name="assignedTo" placeholder="Nome do responsável" data-testid="input-task-assigned" />
</div>
<div className="space-y-2">
<Label htmlFor="task-dueDate">Data limite (opcional)</Label>
<Input id="task-dueDate" name="dueDate" type="date" data-testid="input-task-dueDate" />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowNewTaskDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={addProjectTaskMutation.isPending} data-testid="btn-submit-task">
{addProjectTaskMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar Tarefa
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Global Process Dialogs */}
<Dialog open={showNewProcessDialog} onOpenChange={(open) => { setShowNewProcessDialog(open); if (!open) setNewProcessCategory(""); }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Processo</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createProcessMutation.mutate({
name: formData.get("name") as string,
description: formData.get("description") as string,
category: newProcessCategory || undefined,
owner: formData.get("owner") as string,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="process-name">Nome do Processo</Label>
<Input id="process-name" name="name" required placeholder="Ex: Processo de Vendas" data-testid="input-process-name" />
</div>
<div className="space-y-2">
<Label htmlFor="process-description">Descrição</Label>
<Textarea id="process-description" name="description" placeholder="Descreva o processo..." data-testid="input-process-description" />
</div>
<div className="space-y-2">
<Label htmlFor="process-category">Categoria</Label>
<Select value={newProcessCategory} onValueChange={setNewProcessCategory}>
<SelectTrigger data-testid="select-process-category">
<SelectValue placeholder="Selecione uma categoria" />
</SelectTrigger>
<SelectContent>
{PROCESS_CATEGORIES.map(cat => (
<SelectItem key={cat.id} value={cat.id}>{cat.label}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="process-owner">Responsável</Label>
<Input id="process-owner" name="owner" placeholder="Nome do responsável" data-testid="input-process-owner" />
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowNewProcessDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={createProcessMutation.isPending} data-testid="btn-submit-process">
{createProcessMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showProcessDiagramDialog} onOpenChange={setShowProcessDiagramDialog}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>
Diagrama de Processo: {processes.find((p: PcProcess) => p.id === diagramProcessId)?.name}
</DialogTitle>
</DialogHeader>
<ProcessDiagram
key={diagramProcessId}
initialNodes={processes.find((p: PcProcess) => p.id === diagramProcessId)?.diagramNodes || []}
initialEdges={processes.find((p: PcProcess) => p.id === diagramProcessId)?.diagramEdges || []}
initialViewport={processes.find((p: PcProcess) => p.id === diagramProcessId)?.diagramViewport}
onSave={async (nodes, edges, viewport) => {
if (!diagramProcessId) return;
try {
const res = await fetch(`/api/compass/processes/${diagramProcessId}/diagram`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ diagramNodes: nodes, diagramEdges: edges, diagramViewport: viewport }),
});
if (res.ok) {
toast.success("Diagrama salvo com sucesso!");
queryClient.invalidateQueries({ queryKey: ["/api/compass/projects", selectedProjectId, "processes"] });
} else {
toast.error("Erro ao salvar diagrama");
}
} catch (error) {
toast.error("Erro ao salvar diagrama");
}
}}
/>
</DialogContent>
</Dialog>
{/* Report Configuration Modal */}
<Dialog open={showReportConfigModal} onOpenChange={setShowReportConfigModal}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-xl">Configurar Relatório</DialogTitle>
<p className="text-sm text-muted-foreground">
{reportConfigTemplate === "executive_summary" ? "Sumário Executivo" :
reportConfigTemplate === "full_diagnostic" ? "Diagnóstico Completo" :
"Análise SWOT"} - {selectedProject?.name || "Projeto"}
</p>
</DialogHeader>
<div className="grid grid-cols-3 gap-6 py-4">
{/* Left - Section Selection */}
<div className="col-span-2 space-y-4">
<div>
<h4 className="font-medium mb-2">Seções do Relatório</h4>
<p className="text-sm text-muted-foreground mb-4">Selecione as seções que deseja incluir</p>
</div>
<div className="space-y-2">
{REPORT_SECTIONS.map(section => (
<div
key={section.id}
className={`flex items-center gap-3 p-4 border rounded-lg cursor-pointer transition-colors ${
reportConfigSections.includes(section.id) ? "border-primary bg-primary/5" : "hover:bg-muted/50"
}`}
onClick={() => toggleReportSection(section.id)}
>
<Checkbox
checked={reportConfigSections.includes(section.id)}
onCheckedChange={() => toggleReportSection(section.id)}
/>
<div className="flex-1">
<p className="font-medium">{section.label}</p>
<p className="text-sm text-muted-foreground">{section.description}</p>
</div>
</div>
))}
</div>
</div>
{/* Right - Summary */}
<div className="space-y-4">
<Card className="p-4">
<h4 className="font-medium mb-3">Resumo</h4>
<div className="space-y-3">
<div>
<p className="text-sm text-muted-foreground">Template</p>
<p className="font-medium">
{reportConfigTemplate === "executive_summary" ? "Sumário Executivo" :
reportConfigTemplate === "full_diagnostic" ? "Diagnóstico Completo" :
"Análise SWOT"}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Seções selecionadas</p>
<p className="font-medium">{reportConfigSections.length} de {REPORT_SECTIONS.length}</p>
</div>
<div className="flex flex-wrap gap-1 mt-2">
{reportConfigSections.map(sectionId => {
const section = REPORT_SECTIONS.find(s => s.id === sectionId);
return section ? (
<Badge key={sectionId} variant="secondary" className="text-xs">
{section.label}
</Badge>
) : null;
})}
</div>
</div>
</Card>
<Button
className="w-full"
onClick={() => {
if (!selectedProjectId) return;
const templateNames: Record<string, string> = {
executive_summary: "Sumário Executivo",
full_diagnostic: "Diagnóstico Completo",
swot_report: "Análise SWOT",
};
const project = projects.find(p => p.id === selectedProjectId);
const name = `${templateNames[reportConfigTemplate] || reportConfigTemplate} - ${project?.name || "Projeto"}`;
generateReportMutation.mutate({
projectId: selectedProjectId,
name,
templateType: reportConfigTemplate,
format: "pdf",
sections: reportConfigSections
});
setShowReportConfigModal(false);
}}
disabled={reportConfigSections.length === 0 || generateReportMutation.isPending}
data-testid="btn-generate-configured-report"
>
{generateReportMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
<Eye className="h-4 w-4 mr-2" /> Visualizar Relatório
</Button>
<Button
variant="outline"
className="w-full"
onClick={() => {
toast.success("Configuração salva!");
}}
data-testid="btn-save-report-config"
>
<Save className="h-4 w-4 mr-2" /> Salvar Configuração
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Report Editor Modal */}
<Dialog open={showReportEditorDialog} onOpenChange={setShowReportEditorDialog}>
<DialogContent className="max-w-5xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<div className="flex items-center justify-between">
<div className="flex-1">
<Input
value={reportName}
onChange={(e) => setReportName(e.target.value)}
className="text-lg font-semibold border-none shadow-none px-0 focus-visible:ring-0"
placeholder="Nome do relatório..."
/>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={exportReportAsPDF}>
<FileText className="h-4 w-4 mr-1" /> PDF
</Button>
<Button
size="sm"
onClick={saveReportContent}
disabled={reportIsSaving}
>
{reportIsSaving ? <Loader2 className="h-4 w-4 mr-1 animate-spin" /> : <Save className="h-4 w-4 mr-1" />}
Salvar
</Button>
</div>
</div>
</DialogHeader>
<div className="flex-1 overflow-auto border rounded-lg mt-4">
<div
className="min-h-[500px] p-6 prose prose-sm max-w-none"
style={{ fontFamily: "'Segoe UI', Arial, sans-serif" }}
>
{reportContent ? (
<div
contentEditable
dangerouslySetInnerHTML={{ __html: reportContent }}
onBlur={(e) => setReportContent(e.currentTarget.innerHTML)}
className="outline-none focus:outline-none min-h-full"
style={{
lineHeight: 1.6,
}}
/>
) : (
<div className="text-center py-12 text-muted-foreground">
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
<p>Nenhum conteúdo gerado ainda</p>
<p className="text-sm">Gere um relatório para visualizar e editar aqui</p>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
</BrowserFrame>
);
}
// Valuation Section Component
function ValuationSection({ tenantId }: { tenantId?: number }) {
const queryClient = useQueryClient();
const [selectedProject, setSelectedProject] = useState<any>(null);
const [showNewProjectDialog, setShowNewProjectDialog] = useState(false);
const [showValuationProjectDialog, setShowValuationProjectDialog] = useState(false);
const [activeValuationTab, setActiveValuationTab] = useState("overview");
const { data: valuationProjects = [], isLoading } = useQuery<any[]>({
queryKey: ["/api/valuation/projects"],
queryFn: async () => {
const res = await fetch("/api/valuation/projects");
if (!res.ok) throw new Error("Failed to fetch projects");
return res.json();
},
enabled: !!tenantId,
});
// Auto-select first project when data loads
useEffect(() => {
if (valuationProjects.length > 0 && !selectedProject) {
setSelectedProject(valuationProjects[0]);
}
}, [valuationProjects, selectedProject]);
const { data: crmClients = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/crm-clients"],
queryFn: async () => {
const res = await fetch("/api/valuation/crm-clients");
if (!res.ok) return [];
return res.json();
},
enabled: !!tenantId,
});
const { data: sectorAnalysis } = useQuery<any>({
queryKey: ["/api/valuation/projects", selectedProject?.id, "sector-analysis"],
queryFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/sector-analysis`);
return res.json();
},
enabled: !!selectedProject?.id,
});
const { data: canvasBlocks = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/projects", selectedProject?.id, "canvas"],
queryFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/canvas`);
return res.json();
},
enabled: !!selectedProject?.id,
});
const { data: checklistProgress = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/projects", selectedProject?.id, "checklist"],
queryFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/checklist`);
return res.json();
},
enabled: !!selectedProject?.id,
});
const { data: checklistCategories = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/checklist/categories", selectedProject?.sector],
queryFn: async () => {
const segment = selectedProject?.sector === "Indústria" ? "industry" :
selectedProject?.sector === "Serviços" ? "services" :
selectedProject?.sector === "Varejo" ? "retail" :
selectedProject?.sector === "Tecnologia" ? "technology" : "general";
const res = await fetch(`/api/valuation/checklist/categories?segment=${segment}`);
return res.json();
},
enabled: !!selectedProject?.id,
});
const { data: checklistItems = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/checklist/items", selectedProject?.sector],
queryFn: async () => {
const segment = selectedProject?.sector === "Indústria" ? "industry" :
selectedProject?.sector === "Serviços" ? "services" :
selectedProject?.sector === "Varejo" ? "retail" :
selectedProject?.sector === "Tecnologia" ? "technology" : "general";
const res = await fetch(`/api/valuation/checklist/items?segment=${segment}`);
return res.json();
},
enabled: !!selectedProject?.id,
});
const { data: financialInputs = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/projects", selectedProject?.id, "inputs"],
queryFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/inputs`);
return res.json();
},
enabled: !!selectedProject?.id,
});
const { data: documents = [] } = useQuery<any[]>({
queryKey: ["/api/valuation/projects", selectedProject?.id, "documents"],
queryFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/documents`);
return res.json();
},
enabled: !!selectedProject?.id,
});
const calculateSectorMutation = useMutation({
mutationFn: async () => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/sector-analysis/calculate`, {
method: "POST",
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/valuation/projects", selectedProject?.id, "sector-analysis"] });
},
});
const createValuationProjectMutation = useMutation({
mutationFn: async (data: any) => {
const res = await fetch("/api/valuation/projects", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify(data),
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/valuation/projects"] });
setShowValuationProjectDialog(false);
},
});
const saveCanvasBlock = useMutation({
mutationFn: async ({ blockType, data }: { blockType: string; data: any }) => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/canvas/${blockType}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/valuation/projects", selectedProject?.id, "canvas"] });
},
});
const initializeChecklistMutation = useMutation({
mutationFn: async () => {
const segment = selectedProject?.sector === "Indústria" ? "industry" :
selectedProject?.sector === "Serviços" ? "services" :
selectedProject?.sector === "Varejo" ? "retail" :
selectedProject?.sector === "Tecnologia" ? "technology" : "general";
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/checklist/initialize`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ segment }),
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/valuation/projects", selectedProject?.id, "checklist"] });
},
});
const updateChecklistItemMutation = useMutation({
mutationFn: async ({ itemId, status, notes, value }: { itemId: number; status: string; notes?: string; value?: string }) => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/checklist/${itemId}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ status, notes, value }),
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/valuation/projects", selectedProject?.id, "checklist"] });
},
});
const saveFinancialInputMutation = useMutation({
mutationFn: async (data: any) => {
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/inputs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({
...data,
value: parseFloat(data.value) || 0,
}),
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/valuation/projects", selectedProject?.id, "inputs"] });
},
});
const uploadDocumentMutation = useMutation({
mutationFn: async (file: File) => {
const formData = new FormData();
formData.append("file", file);
formData.append("documentType", "other");
const res = await fetch(`/api/valuation/projects/${selectedProject.id}/documents/upload`, {
method: "POST",
credentials: "include",
body: formData,
});
return res.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["/api/valuation/projects", selectedProject?.id, "documents"] });
},
});
const [showFinancialDialog, setShowFinancialDialog] = useState(false);
const [expandedCategories, setExpandedCategories] = useState<string[]>([]);
const CANVAS_BLOCKS = [
{ id: "customer_segments", label: "Segmentos de Clientes", description: "Para quem criamos valor?" },
{ id: "value_proposition", label: "Proposta de Valor", description: "Que valor entregamos?" },
{ id: "channels", label: "Canais", description: "Como alcançamos nossos clientes?" },
{ id: "customer_relationships", label: "Relacionamento", description: "Como nos relacionamos?" },
{ id: "revenue_streams", label: "Fontes de Receita", description: "Como geramos receita?" },
{ id: "key_resources", label: "Recursos Principais", description: "Quais recursos são essenciais?" },
{ id: "key_activities", label: "Atividades Principais", description: "O que fazemos de mais importante?" },
{ id: "key_partnerships", label: "Parcerias Principais", description: "Quem são nossos parceiros?" },
{ id: "cost_structure", label: "Estrutura de Custos", description: "Quais são nossos custos?" },
];
const getBlockData = (blockType: string) => {
return canvasBlocks.find((b: any) => b.blockType === blockType);
};
const getScoreLevel = (score: number) => {
if (score >= 80) return { label: "Excelente", color: "text-emerald-600" };
if (score >= 60) return { label: "Bom", color: "text-blue-600" };
if (score >= 40) return { label: "Intermediário", color: "text-yellow-600" };
return { label: "Inicial", color: "text-red-600" };
};
if (!tenantId) {
return (
<Card className="p-8 text-center">
<TrendingUp className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">Crie uma organização primeiro para acessar o módulo de Valuation</p>
</Card>
);
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-2xl font-bold flex items-center gap-2">
<TrendingUp className="h-6 w-6 text-primary" />
Valuation
</h2>
<p className="text-muted-foreground">Avaliação de empresas e análise setorial</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
{/* Projects List */}
<Card className="lg:col-span-1">
<CardHeader className="py-3">
<CardTitle className="text-sm">Projetos de Valuation</CardTitle>
</CardHeader>
<CardContent className="p-0">
<ScrollArea className="h-[500px]">
{isLoading ? (
<div className="p-4 text-center">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</div>
) : valuationProjects.length === 0 ? (
<div className="p-4 text-center text-muted-foreground text-sm">
Nenhum projeto de valuation
</div>
) : (
<div className="divide-y">
{valuationProjects.map((project: any) => (
<div
key={project.id}
className={`p-3 cursor-pointer hover:bg-muted/50 transition-colors ${
selectedProject?.id === project.id ? "bg-muted" : ""
}`}
onClick={() => setSelectedProject(project)}
data-testid={`valuation-project-${project.id}`}
>
<p className="font-medium text-sm truncate">{project.companyName}</p>
<p className="text-xs text-muted-foreground">{project.sector}</p>
<Badge variant="outline" className="mt-1 text-xs">
{project.status || "Em andamento"}
</Badge>
</div>
))}
</div>
)}
</ScrollArea>
</CardContent>
</Card>
{/* Project Details */}
<div className="lg:col-span-3">
{selectedProject ? (
<Card>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle>{selectedProject.companyName}</CardTitle>
<CardDescription>{selectedProject.sector} | {selectedProject.size}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<Tabs value={activeValuationTab} onValueChange={setActiveValuationTab}>
<TabsList className="mb-4 flex-wrap">
<TabsTrigger value="overview">Visão Geral</TabsTrigger>
<TabsTrigger value="checklist">Checklist</TabsTrigger>
<TabsTrigger value="financial">Financeiro</TabsTrigger>
<TabsTrigger value="documents">Documentos</TabsTrigger>
<TabsTrigger value="sector">Análise Setorial</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<div className="grid grid-cols-3 gap-4">
<Card className="p-4">
<p className="text-sm text-muted-foreground">Checklist</p>
<p className="text-2xl font-bold">
{checklistProgress.filter((c: any) => c.status === "completed").length}/{checklistProgress.length}
</p>
<p className="text-xs text-muted-foreground">itens completos</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">Score Setorial</p>
<p className="text-2xl font-bold">{sectorAnalysis?.latest?.overallScore || "--"}%</p>
<p className="text-xs text-muted-foreground">
{sectorAnalysis?.latest?.overallScore
? getScoreLevel(sectorAnalysis.latest.overallScore).label
: "Não calculado"}
</p>
</Card>
<Card className="p-4">
<p className="text-sm text-muted-foreground">Canvas</p>
<p className="text-2xl font-bold">{canvasBlocks.length}/9</p>
<p className="text-xs text-muted-foreground">blocos preenchidos</p>
</Card>
</div>
</TabsContent>
<TabsContent value="checklist">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-medium">Checklist de Coleta de Dados</h3>
{checklistProgress.length === 0 && (
<Button
onClick={() => initializeChecklistMutation.mutate()}
disabled={initializeChecklistMutation.isPending}
data-testid="btn-initialize-checklist"
>
{initializeChecklistMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Plus className="h-4 w-4 mr-2" />
)}
Inicializar Checklist
</Button>
)}
</div>
{checklistProgress.length === 0 ? (
<Card className="p-8 text-center">
<ClipboardList className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">Clique em "Inicializar Checklist" para começar</p>
<p className="text-sm text-muted-foreground mt-1">
O checklist será criado com base no setor da empresa
</p>
</Card>
) : (
<div className="space-y-3">
{/* Progress Bar */}
<div className="flex items-center gap-4 p-4 bg-muted/30 rounded-lg">
<div className="flex-1">
<div className="flex justify-between text-sm mb-1">
<span>Progresso</span>
<span>{checklistProgress.filter((c: any) => c.status === "completed").length} de {checklistProgress.length}</span>
</div>
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all"
style={{ width: `${(checklistProgress.filter((c: any) => c.status === "completed").length / checklistProgress.length) * 100}%` }}
/>
</div>
</div>
</div>
{/* Checklist Items */}
<Accordion type="multiple" value={expandedCategories} onValueChange={setExpandedCategories}>
{checklistCategories.map((category: any) => {
const categoryItems = checklistProgress.filter((p: any) =>
checklistItems.find((i: any) => i.id === p.itemId && i.categoryId === category.id)
);
const completedCount = categoryItems.filter((p: any) => p.status === "completed").length;
return (
<AccordionItem key={category.id} value={String(category.id)}>
<AccordionTrigger className="hover:no-underline">
<div className="flex items-center gap-3 w-full">
<div className="flex-1 text-left">
<span className="font-medium">{category.name}</span>
<span className="text-xs text-muted-foreground ml-2">
({completedCount}/{categoryItems.length})
</span>
</div>
{category.weight && (
<Badge variant="outline" className="text-xs">
Peso: {category.weight}%
</Badge>
)}
</div>
</AccordionTrigger>
<AccordionContent>
<div className="space-y-2 pl-4">
{checklistItems
.filter((item: any) => item.categoryId === category.id)
.map((item: any) => {
const progress = checklistProgress.find((p: any) => p.itemId === item.id);
const isCompleted = progress?.status === "completed";
return (
<div
key={item.id}
className={`p-3 border rounded-lg ${isCompleted ? "bg-emerald-50 border-emerald-200" : "bg-background"}`}
>
<div className="flex items-start gap-3">
<Checkbox
checked={isCompleted}
onCheckedChange={(checked) => {
updateChecklistItemMutation.mutate({
itemId: item.id,
status: checked ? "completed" : "pending",
notes: progress?.notes,
value: progress?.value,
});
}}
data-testid={`checklist-item-${item.id}`}
/>
<div className="flex-1">
<p className={`text-sm ${isCompleted ? "line-through text-muted-foreground" : ""}`}>
{item.name}
</p>
{item.description && (
<p className="text-xs text-muted-foreground mt-1">{item.description}</p>
)}
{item.required && (
<Badge variant="destructive" className="text-xs mt-1">Obrigatório</Badge>
)}
</div>
</div>
</div>
);
})}
</div>
</AccordionContent>
</AccordionItem>
);
})}
</Accordion>
</div>
)}
</div>
</TabsContent>
<TabsContent value="financial">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-medium">Dados Financeiros</h3>
<Button onClick={() => setShowFinancialDialog(true)} data-testid="btn-add-financial">
<Plus className="h-4 w-4 mr-2" /> Adicionar Dados
</Button>
</div>
{financialInputs.length === 0 ? (
<Card className="p-8 text-center">
<BarChart3 className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">Nenhum dado financeiro cadastrado</p>
<p className="text-sm text-muted-foreground mt-1">
Adicione receitas, custos, EBITDA e outros indicadores
</p>
</Card>
) : (
<div className="space-y-3">
<div className="grid grid-cols-4 gap-3 text-sm font-medium text-muted-foreground border-b pb-2">
<span>Indicador</span>
<span>Período</span>
<span className="text-right">Valor</span>
<span>Fonte</span>
</div>
{financialInputs.map((input: any) => (
<div key={input.id} className="grid grid-cols-4 gap-3 text-sm py-2 border-b">
<span className="font-medium">{input.inputType}</span>
<span>{input.period}</span>
<span className="text-right font-mono">
{new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(input.value))}
</span>
<Badge variant="outline" className="text-xs w-fit">{input.source || "Manual"}</Badge>
</div>
))}
</div>
)}
</div>
</TabsContent>
<TabsContent value="documents">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-medium">Documentos e Anexos</h3>
<label htmlFor="file-upload">
<input
id="file-upload"
type="file"
className="hidden"
accept=".pdf,.xls,.xlsx,.csv,.json,.xml,.jpg,.jpeg,.png"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) uploadDocumentMutation.mutate(file);
}}
data-testid="input-file-upload"
/>
<Button asChild disabled={uploadDocumentMutation.isPending}>
<span>
{uploadDocumentMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<Plus className="h-4 w-4 mr-2" />
)}
Upload
</span>
</Button>
</label>
</div>
{documents.length === 0 ? (
<Card className="p-8 text-center">
<FileText className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">Nenhum documento anexado</p>
<p className="text-sm text-muted-foreground mt-1">
Faça upload de demonstrativos, contratos e outros documentos
</p>
</Card>
) : (
<div className="grid grid-cols-2 gap-3">
{documents.map((doc: any) => (
<Card key={doc.id} className="p-3 flex items-center gap-3" data-testid={`document-${doc.id}`}>
<div className="h-10 w-10 bg-muted rounded flex items-center justify-center">
<FileText className="h-5 w-5 text-muted-foreground" />
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{doc.name}</p>
<p className="text-xs text-muted-foreground">{doc.folder || doc.fileType}</p>
</div>
<Button variant="ghost" size="icon" asChild>
<a href={`/api/valuation/projects/${selectedProject.id}/documents/${doc.id}/download`} download>
<Download className="h-4 w-4" />
</a>
</Button>
</Card>
))}
</div>
)}
</div>
</TabsContent>
<TabsContent value="sector">
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-medium">Análise Setorial com Pesos</h3>
<Button
onClick={() => calculateSectorMutation.mutate()}
disabled={calculateSectorMutation.isPending}
data-testid="btn-calculate-sector"
>
{calculateSectorMutation.isPending ? (
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
) : (
<RefreshCw className="h-4 w-4 mr-2" />
)}
Calcular Análise
</Button>
</div>
{sectorAnalysis?.latest ? (
<div className="grid grid-cols-2 gap-6">
{/* Score Circular */}
<Card className="p-6 bg-slate-900 text-white">
<p className="text-sm text-slate-400 mb-4">Score Geral de Maturidade</p>
<div className="flex items-center justify-center">
<div className="relative w-40 h-40">
<svg className="w-40 h-40 transform -rotate-90">
<circle
cx="80"
cy="80"
r="70"
fill="none"
stroke="#334155"
strokeWidth="12"
/>
<circle
cx="80"
cy="80"
r="70"
fill="none"
stroke="#eab308"
strokeWidth="12"
strokeLinecap="round"
strokeDasharray={`${(sectorAnalysis.latest.overallScore / 100) * 440} 440`}
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-4xl font-bold">{sectorAnalysis.latest.overallScore}</span>
</div>
</div>
</div>
<div className="mt-4 text-center">
<p className="font-medium text-lg">{getScoreLevel(sectorAnalysis.latest.overallScore).label}</p>
<p className="text-sm text-slate-400">
{sectorAnalysis.latest.overallScore >= 60
? "A empresa possui processos definidos"
: "A empresa carece de otimização"}
</p>
</div>
</Card>
{/* Pontos fortes/fracos */}
<div className="space-y-4">
{sectorAnalysis.latest.strengths?.length > 0 && (
<Card className="p-4">
<p className="font-medium text-sm text-emerald-600 mb-2 flex items-center gap-1">
<CheckCircle className="h-4 w-4" /> Pontos Fortes
</p>
<ul className="space-y-1 text-sm">
{sectorAnalysis.latest.strengths.map((s: string, i: number) => (
<li key={i} className="text-muted-foreground"> {s}</li>
))}
</ul>
</Card>
)}
{sectorAnalysis.latest.weaknesses?.length > 0 && (
<Card className="p-4">
<p className="font-medium text-sm text-red-600 mb-2 flex items-center gap-1">
<AlertCircle className="h-4 w-4" /> Pontos de Atenção
</p>
<ul className="space-y-1 text-sm">
{sectorAnalysis.latest.weaknesses.map((w: string, i: number) => (
<li key={i} className="text-muted-foreground"> {w}</li>
))}
</ul>
</Card>
)}
{sectorAnalysis.latest.recommendations?.length > 0 && (
<Card className="p-4">
<p className="font-medium text-sm text-blue-600 mb-2 flex items-center gap-1">
<Lightbulb className="h-4 w-4" /> Recomendações
</p>
<ul className="space-y-1 text-sm">
{sectorAnalysis.latest.recommendations.map((r: string, i: number) => (
<li key={i} className="text-muted-foreground"> {r}</li>
))}
</ul>
</Card>
)}
</div>
</div>
) : (
<Card className="p-8 text-center">
<BarChart3 className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">Clique em "Calcular Análise" para gerar o score setorial</p>
<p className="text-sm text-muted-foreground mt-1">
A análise considera o progresso do checklist, dados financeiros e maturidade
</p>
</Card>
)}
</div>
</TabsContent>
</Tabs>
</CardContent>
</Card>
) : (
<Card className="p-8 text-center h-full flex items-center justify-center">
<div>
<TrendingUp className="h-12 w-12 mx-auto mb-4 text-muted-foreground" />
<p className="text-muted-foreground">Selecione um projeto para ver os detalhes</p>
</div>
</Card>
)}
</div>
</div>
{/* Valuation Project Dialog */}
<Dialog open={showValuationProjectDialog} onOpenChange={setShowValuationProjectDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Novo Projeto de Valuation</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createValuationProjectMutation.mutate({
companyName: formData.get("companyName"),
sector: formData.get("sector"),
size: formData.get("size"),
stage: formData.get("stage") || "Growth",
clientId: formData.get("clientId") ? Number(formData.get("clientId")) : undefined,
});
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Cliente (CRM)</Label>
<Select name="clientId">
<SelectTrigger data-testid="select-valuation-client">
<SelectValue placeholder="Selecione um cliente..." />
</SelectTrigger>
<SelectContent>
{crmClients.map((client: any) => (
<SelectItem key={client.id} value={String(client.id)}>
{client.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Nome da Empresa</Label>
<Input name="companyName" placeholder="Nome da empresa" required data-testid="input-valuation-company" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Setor</Label>
<Select name="sector">
<SelectTrigger data-testid="select-valuation-sector">
<SelectValue placeholder="Setor..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="Indústria">Indústria</SelectItem>
<SelectItem value="Serviços">Serviços</SelectItem>
<SelectItem value="Varejo">Varejo</SelectItem>
<SelectItem value="Tecnologia">Tecnologia</SelectItem>
<SelectItem value="Agronegócio">Agronegócio</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Porte</Label>
<Select name="size">
<SelectTrigger data-testid="select-valuation-size">
<SelectValue placeholder="Porte..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="Micro">Micro</SelectItem>
<SelectItem value="Pequena">Pequena</SelectItem>
<SelectItem value="Média">Média</SelectItem>
<SelectItem value="Grande">Grande</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowValuationProjectDialog(false)}>
Cancelar
</Button>
<Button type="submit" disabled={createValuationProjectMutation.isPending} data-testid="btn-create-valuation-project">
{createValuationProjectMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Criar Projeto
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
{/* Financial Input Dialog */}
<Dialog open={showFinancialDialog} onOpenChange={setShowFinancialDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle>Adicionar Dados Financeiros</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
saveFinancialInputMutation.mutate({
inputType: formData.get("inputType"),
period: formData.get("period"),
value: formData.get("value"),
source: formData.get("source") || "manual",
});
setShowFinancialDialog(false);
}}>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Tipo de Indicador</Label>
<Select name="inputType">
<SelectTrigger data-testid="select-financial-type">
<SelectValue placeholder="Selecione..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="revenue">Receita</SelectItem>
<SelectItem value="gross_revenue">Receita Bruta</SelectItem>
<SelectItem value="net_revenue">Receita Líquida</SelectItem>
<SelectItem value="cogs">Custo (CMV)</SelectItem>
<SelectItem value="gross_profit">Lucro Bruto</SelectItem>
<SelectItem value="operating_expenses">Despesas Operacionais</SelectItem>
<SelectItem value="ebitda">EBITDA</SelectItem>
<SelectItem value="net_income">Lucro Líquido</SelectItem>
<SelectItem value="total_assets">Ativos Totais</SelectItem>
<SelectItem value="total_liabilities">Passivos Totais</SelectItem>
<SelectItem value="equity">Patrimônio Líquido</SelectItem>
<SelectItem value="working_capital">Capital de Giro</SelectItem>
<SelectItem value="capex">CAPEX</SelectItem>
<SelectItem value="debt">Dívida</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Período</Label>
<Select name="period">
<SelectTrigger data-testid="select-financial-period">
<SelectValue placeholder="Período..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2023">2023</SelectItem>
<SelectItem value="2022">2022</SelectItem>
<SelectItem value="2021">2021</SelectItem>
<SelectItem value="2020">2020</SelectItem>
<SelectItem value="TTM">Últimos 12 meses</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Fonte</Label>
<Select name="source">
<SelectTrigger data-testid="select-financial-source">
<SelectValue placeholder="Fonte..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="manual">Manual</SelectItem>
<SelectItem value="erp">ERP</SelectItem>
<SelectItem value="excel">Planilha</SelectItem>
<SelectItem value="accounting">Contabilidade</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>Valor (R$)</Label>
<Input
name="value"
type="number"
step="0.01"
placeholder="0,00"
required
data-testid="input-financial-value"
/>
</div>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setShowFinancialDialog(false)}>
Cancelar
</Button>
<Button type="submit" disabled={saveFinancialInputMutation.isPending} data-testid="btn-save-financial">
{saveFinancialInputMutation.isPending && <Loader2 className="h-4 w-4 mr-2 animate-spin" />}
Salvar
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</div>
);
}