arcadiasuite/client/src/pages/CommercialEnv.tsx

1149 lines
55 KiB
TypeScript

import { useState, useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { BrowserFrame } from "@/components/Browser/BrowserFrame";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { useToast } from "@/hooks/use-toast";
import {
FileText, Users, TrendingUp, DollarSign, Plus, Search, Edit, Eye,
RefreshCw, CheckCircle, XCircle, Clock, Send, ArrowRight, Building2,
Loader2, MapPin, Calendar, Target, FileCheck, Rocket, BarChart3
} from "lucide-react";
type TabType = "dashboard" | "customers" | "proposals" | "projects" | "services" | "pipeline";
interface ArcadiaCustomer {
name: string;
customer_name?: string;
territory?: string;
customer_group?: string;
}
interface ArcadiaSalesOrder {
name: string;
customer: string;
customer_name: string;
transaction_date: string;
delivery_date: string;
grand_total: number;
status: string;
per_delivered: number;
per_billed: number;
}
interface Proposal {
id: number;
proposalNumber: string;
title: string;
customerId: string;
customerName: string;
serviceType: string;
status: string;
totalValue: number;
validUntil: string;
createdAt: string;
}
interface EnvironmentalService {
id: number;
code?: string;
name: string;
description?: string;
category?: string;
basePrice?: string;
unit?: string;
estimatedDuration?: number;
items?: string[];
isActive?: number;
}
const defaultServices: EnvironmentalService[] = [
{ id: -1, code: "MON-001", name: "Monitoramento de Águas Subterrâneas", description: "Campanha de monitoramento de poços com análises laboratoriais", category: "Monitoramento", basePrice: "15000", items: ["Mobilização de equipe", "Coleta de amostras", "Análises laboratoriais", "Relatório técnico"] },
{ id: -2, code: "INV-001", name: "Investigação Confirmatória", description: "Investigação de áreas potencialmente contaminadas conforme CONAMA 420", category: "Investigação", basePrice: "45000", items: ["Sondagens", "Instalação de poços", "Coleta de amostras", "Análises laboratoriais", "Modelo conceitual", "Relatório técnico"] },
{ id: -3, code: "INV-002", name: "Investigação Detalhada", description: "Delimitação de plumas de contaminação e avaliação de risco", category: "Investigação", basePrice: "85000", items: ["Sondagens adicionais", "Poços multinível", "Slug tests", "Análises químicas", "Modelagem", "Avaliação de risco", "Relatório técnico"] },
{ id: -4, code: "REM-001", name: "Projeto de Remediação", description: "Elaboração de plano de intervenção para áreas contaminadas", category: "Remediação", basePrice: "35000", items: ["Análise de alternativas", "Dimensionamento", "Projeto executivo", "Cronograma", "Orçamento"] },
{ id: -5, code: "LIC-001", name: "Licenciamento Ambiental", description: "Elaboração de estudos para licenciamento ambiental", category: "Licenciamento", basePrice: "25000", items: ["Diagnóstico ambiental", "Estudos específicos", "Elaboração de EIA/RIMA ou RAS", "Acompanhamento CETESB"] },
{ id: -6, code: "AUD-001", name: "Auditoria Ambiental (Due Diligence)", description: "Avaliação ambiental de imóveis para transações imobiliárias", category: "Consultoria", basePrice: "18000", items: ["Pesquisa documental", "Vistoria técnica", "Avaliação de passivos", "Relatório de due diligence"] },
];
const serviceCategories = ["Monitoramento", "Investigação", "Remediação", "Licenciamento", "Consultoria", "Outro"];
const statusColors: Record<string, string> = {
rascunho: "bg-gray-500",
enviada: "bg-blue-500",
visualizada: "bg-purple-500",
em_negociacao: "bg-yellow-500",
aceita: "bg-green-500",
rejeitada: "bg-red-500",
expirada: "bg-gray-400",
Completed: "bg-green-500",
"To Deliver and Bill": "bg-yellow-500",
"To Bill": "bg-blue-500",
"To Deliver": "bg-orange-500",
Draft: "bg-gray-500",
};
const api = {
get: async (url: string) => {
const res = await fetch(url, { credentials: "include" });
if (!res.ok) throw new Error("Request failed");
return res.json();
},
post: async (url: string, data: any) => {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Request failed");
return res.json();
},
put: async (url: string, data: any) => {
const res = await fetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
credentials: "include",
});
if (!res.ok) throw new Error("Request failed");
return res.json();
},
delete: async (url: string) => {
const res = await fetch(url, { method: "DELETE", credentials: "include" });
if (!res.ok) throw new Error("Request failed");
return res.json();
},
};
export default function CommercialEnv() {
const { toast } = useToast();
const queryClient = useQueryClient();
const [activeTab, setActiveTab] = useState<TabType>("dashboard");
const [searchTerm, setSearchTerm] = useState("");
const [showProposalDialog, setShowProposalDialog] = useState(false);
const [showServiceDialog, setShowServiceDialog] = useState(false);
const [selectedService, setSelectedService] = useState<EnvironmentalService | null>(null);
const [editingService, setEditingService] = useState<EnvironmentalService | null>(null);
const [selectedCustomer, setSelectedCustomer] = useState<string>("");
const [newItems, setNewItems] = useState<string>("");
const [showProjectDialog, setShowProjectDialog] = useState(false);
const [selectedOrder, setSelectedOrder] = useState<ArcadiaSalesOrder | null>(null);
const [projectWorkflow, setProjectWorkflow] = useState<"pre_projeto" | "backlog" | "planejamento" | "execucao">("pre_projeto");
interface ProjectStatus {
orderId: string;
stage: "venda" | "pre_projeto" | "backlog_tecnico" | "planejamento" | "em_execucao";
technicalTeam?: string;
plannedStart?: string;
plannedEnd?: string;
responsiblePerson?: string;
notes?: string;
}
const [projectStatuses, setProjectStatuses] = useState<Record<string, ProjectStatus>>({});
const { data: erpCustomers = [], isLoading: loadingCustomers, refetch: refetchCustomers } = useQuery<ArcadiaCustomer[]>({
queryKey: ["/api/erpnext/customers"],
queryFn: async () => {
const res = await api.get("/api/erpnext/resource/Customer?limit=100");
return res.data || [];
},
});
const { data: salesOrders = [], isLoading: loadingOrders, refetch: refetchOrders } = useQuery<ArcadiaSalesOrder[]>({
queryKey: ["/api/erpnext/sales-orders"],
queryFn: async () => {
const res = await api.get("/api/erpnext/resource/Sales%20Order?limit=50");
if (!res.data) return [];
const orders = await Promise.all(
res.data.slice(0, 10).map(async (o: any) => {
try {
const detail = await api.get(`/api/erpnext/resource/Sales%20Order/${o.name}`);
return detail.data;
} catch {
return null;
}
})
);
return orders.filter(Boolean);
},
});
const { data: proposals = [], refetch: refetchProposals } = useQuery<Proposal[]>({
queryKey: ["/api/crm/proposals"],
queryFn: async () => {
try {
const res = await api.get("/api/crm/proposals");
return res || [];
} catch {
return [];
}
},
});
const createProposalMutation = useMutation({
mutationFn: async (data: any) => {
return api.post("/api/crm/proposals", data);
},
onSuccess: () => {
toast({ title: "Proposta criada com sucesso" });
refetchProposals();
setShowProposalDialog(false);
},
onError: () => toast({ title: "Erro ao criar proposta", variant: "destructive" }),
});
const { data: dbServices = [], isLoading: loadingServices, refetch: refetchServices } = useQuery<EnvironmentalService[]>({
queryKey: ["/api/quality/services"],
queryFn: async () => {
try {
const res = await api.get("/api/quality/services");
return res.data || [];
} catch {
return [];
}
},
});
const allServices = dbServices.length > 0 ? dbServices : defaultServices;
const createServiceMutation = useMutation({
mutationFn: async (data: any) => api.post("/api/quality/services", data),
onSuccess: () => {
toast({ title: "Serviço criado com sucesso" });
refetchServices();
setShowServiceDialog(false);
setEditingService(null);
},
onError: () => toast({ title: "Erro ao criar serviço", variant: "destructive" }),
});
const updateServiceMutation = useMutation({
mutationFn: async ({ id, data }: { id: number; data: any }) => api.put(`/api/quality/services/${id}`, data),
onSuccess: () => {
toast({ title: "Serviço atualizado com sucesso" });
refetchServices();
setShowServiceDialog(false);
setEditingService(null);
},
onError: () => toast({ title: "Erro ao atualizar serviço", variant: "destructive" }),
});
const deleteServiceMutation = useMutation({
mutationFn: async (id: number) => api.delete(`/api/quality/services/${id}`),
onSuccess: () => {
toast({ title: "Serviço excluído com sucesso" });
refetchServices();
},
onError: () => toast({ title: "Erro ao excluir serviço", variant: "destructive" }),
});
const handleSaveService = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const itemsArray = newItems.split("\n").filter(i => i.trim());
const data = {
code: formData.get("code") as string,
name: formData.get("name") as string,
description: formData.get("description") as string,
category: formData.get("category") as string,
basePrice: formData.get("basePrice") as string,
unit: formData.get("unit") as string,
estimatedDuration: Number(formData.get("estimatedDuration")) || null,
items: itemsArray.length > 0 ? itemsArray : null,
isActive: 1,
};
if (editingService && editingService.id > 0) {
updateServiceMutation.mutate({ id: editingService.id, data });
} else {
createServiceMutation.mutate(data);
}
};
const openEditService = (service: EnvironmentalService) => {
setEditingService(service);
setNewItems(service.items?.join("\n") || "");
setShowServiceDialog(true);
};
const openNewService = () => {
setEditingService(null);
setNewItems("");
setShowServiceDialog(true);
};
const getStatusBadge = (status: string) => {
const color = statusColors[status] || "bg-gray-500";
const label = status.replace(/_/g, " ").replace(/^\w/, c => c.toUpperCase());
return <Badge className={`${color} text-white`}>{label}</Badge>;
};
const filteredCustomers = erpCustomers.filter(c =>
c.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
c.customer_name?.toLowerCase().includes(searchTerm.toLowerCase())
);
const totalPipeline = salesOrders.reduce((sum, o) => sum + (o.grand_total || 0), 0);
const completedOrders = salesOrders.filter(o => o.status === "Completed").length;
const pendingOrders = salesOrders.filter(o => o.status !== "Completed").length;
return (
<BrowserFrame>
<div className="p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<TrendingUp className="h-6 w-6 text-blue-600" />
Comercial - Engenharia Ambiental
</h1>
<p className="text-muted-foreground">
Propostas, clientes e pipeline de vendas integrado ao Arcádia SOE
</p>
</div>
<Button onClick={() => refetchCustomers()} variant="outline" size="sm">
<RefreshCw className="h-4 w-4 mr-2" /> Sincronizar
</Button>
</div>
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as TabType)}>
<TabsList className="h-12">
<TabsTrigger value="dashboard" className="gap-2" data-testid="tab-commercial-dashboard">
<BarChart3 className="h-4 w-4" /> Dashboard
</TabsTrigger>
<TabsTrigger value="customers" className="gap-2" data-testid="tab-erp-customers">
<Building2 className="h-4 w-4" /> Clientes
</TabsTrigger>
<TabsTrigger value="proposals" className="gap-2" data-testid="tab-proposals">
<FileText className="h-4 w-4" /> Propostas
</TabsTrigger>
<TabsTrigger value="projects" className="gap-2" data-testid="tab-sales-orders">
<Rocket className="h-4 w-4" /> Pedidos/Projetos
</TabsTrigger>
<TabsTrigger value="services" className="gap-2" data-testid="tab-services">
<Target className="h-4 w-4" /> Serviços
</TabsTrigger>
<TabsTrigger value="pipeline" className="gap-2" data-testid="tab-pipeline">
<TrendingUp className="h-4 w-4" /> Pipeline
</TabsTrigger>
</TabsList>
<TabsContent value="dashboard" className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Clientes Arcádia</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-blue-600">{erpCustomers.length}</div>
<p className="text-xs text-muted-foreground">Total cadastrado</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Pedidos Ativos</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-yellow-600">{pendingOrders}</div>
<p className="text-xs text-muted-foreground">Em andamento</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Pedidos Concluídos</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-green-600">{completedOrders}</div>
<p className="text-xs text-muted-foreground">Finalizados</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">Pipeline Total</CardTitle>
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-purple-600">
{totalPipeline.toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}
</div>
<p className="text-xs text-muted-foreground">Valor em pedidos</p>
</CardContent>
</Card>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="text-lg">Últimos Pedidos</CardTitle>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Pedido</TableHead>
<TableHead>Cliente</TableHead>
<TableHead>Valor</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{salesOrders.slice(0, 5).map((order) => (
<TableRow key={order.name}>
<TableCell className="font-medium">{order.name}</TableCell>
<TableCell className="max-w-[150px] truncate">{order.customer_name}</TableCell>
<TableCell>{order.grand_total?.toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}</TableCell>
<TableCell>{getStatusBadge(order.status)}</TableCell>
</TableRow>
))}
{salesOrders.length === 0 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-muted-foreground">
Nenhum pedido encontrado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">Serviços Mais Vendidos</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{allServices.slice(0, 4).map((service) => (
<div key={service.id} className="flex items-center justify-between p-3 bg-muted/50 rounded-lg">
<div>
<p className="font-medium text-sm">{service.name}</p>
<p className="text-xs text-muted-foreground">{service.category}</p>
</div>
<Badge variant="outline">
{service.basePrice.toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}
</Badge>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="customers" className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar clientes..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-64"
data-testid="search-customers"
/>
</div>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={() => refetchCustomers()}>
<RefreshCw className="h-4 w-4 mr-2" /> Atualizar
</Button>
<Badge variant="secondary" className="h-9 px-3 flex items-center">
<Building2 className="h-4 w-4 mr-1" /> {erpCustomers.length} clientes
</Badge>
</div>
</div>
<Card>
<CardContent className="p-0">
{loadingCustomers ? (
<div className="flex justify-center py-8"><Loader2 className="h-8 w-8 animate-spin" /></div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Código</TableHead>
<TableHead>Nome</TableHead>
<TableHead>Território</TableHead>
<TableHead>Grupo</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredCustomers.map((customer) => (
<TableRow key={customer.name}>
<TableCell className="font-mono text-sm">{customer.name}</TableCell>
<TableCell className="font-medium">{customer.customer_name || customer.name}</TableCell>
<TableCell>{customer.territory || "-"}</TableCell>
<TableCell>{customer.customer_group || "-"}</TableCell>
<TableCell className="text-right">
<Button
variant="outline"
size="sm"
onClick={() => {
setSelectedCustomer(customer.name);
setShowProposalDialog(true);
}}
>
<FileText className="h-4 w-4 mr-1" /> Nova Proposta
</Button>
</TableCell>
</TableRow>
))}
{filteredCustomers.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-muted-foreground">
Nenhum cliente encontrado
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="proposals" className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Propostas Comerciais</h2>
<Button onClick={() => setShowProposalDialog(true)} data-testid="btn-new-proposal">
<Plus className="h-4 w-4 mr-2" /> Nova Proposta
</Button>
</div>
<Card>
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Número</TableHead>
<TableHead>Título</TableHead>
<TableHead>Cliente</TableHead>
<TableHead>Serviço</TableHead>
<TableHead>Valor</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{proposals.map((p: any) => (
<TableRow key={p.id}>
<TableCell className="font-medium">{p.proposalNumber || `PROP-${p.id}`}</TableCell>
<TableCell className="max-w-[200px] truncate">{p.title}</TableCell>
<TableCell>{p.customerName || "-"}</TableCell>
<TableCell>{p.serviceType || "-"}</TableCell>
<TableCell>{(p.totalValue || 0).toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}</TableCell>
<TableCell>{getStatusBadge(p.status || "rascunho")}</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="icon"><Eye className="h-4 w-4" /></Button>
<Button variant="ghost" size="icon"><Send className="h-4 w-4" /></Button>
</TableCell>
</TableRow>
))}
{proposals.length === 0 && (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
Nenhuma proposta cadastrada. Clique em "Nova Proposta" para começar.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="projects" className="space-y-4">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Pedidos de Venda Projetos</h2>
<p className="text-sm text-muted-foreground">Gerencie o fluxo: Venda Pré-Projeto Backlog Técnico Planejamento Execução</p>
</div>
<Button variant="outline" size="sm" onClick={() => refetchOrders()}>
<RefreshCw className="h-4 w-4 mr-2" /> Sincronizar
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-5 gap-2 mb-4">
<Card className="border-l-4 border-l-blue-500">
<CardContent className="pt-4 pb-2">
<p className="text-2xl font-bold">{salesOrders.length}</p>
<p className="text-xs text-muted-foreground">Vendas</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-purple-500">
<CardContent className="pt-4 pb-2">
<p className="text-2xl font-bold">{Object.values(projectStatuses).filter(p => p.stage === "pre_projeto").length}</p>
<p className="text-xs text-muted-foreground">Pré-Projetos</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-yellow-500">
<CardContent className="pt-4 pb-2">
<p className="text-2xl font-bold">{Object.values(projectStatuses).filter(p => p.stage === "backlog_tecnico").length}</p>
<p className="text-xs text-muted-foreground">Backlog Técnico</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-orange-500">
<CardContent className="pt-4 pb-2">
<p className="text-2xl font-bold">{Object.values(projectStatuses).filter(p => p.stage === "planejamento").length}</p>
<p className="text-xs text-muted-foreground">Em Planejamento</p>
</CardContent>
</Card>
<Card className="border-l-4 border-l-green-500">
<CardContent className="pt-4 pb-2">
<p className="text-2xl font-bold">{Object.values(projectStatuses).filter(p => p.stage === "em_execucao").length}</p>
<p className="text-xs text-muted-foreground">Em Execução</p>
</CardContent>
</Card>
</div>
<Card>
<CardContent className="p-0">
{loadingOrders ? (
<div className="flex justify-center py-8"><Loader2 className="h-8 w-8 animate-spin" /></div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Pedido</TableHead>
<TableHead>Cliente</TableHead>
<TableHead>Valor Total</TableHead>
<TableHead>Etapa do Projeto</TableHead>
<TableHead>Entrega</TableHead>
<TableHead>Status ERP</TableHead>
<TableHead className="text-right">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{salesOrders.map((order) => {
const projectStatus = projectStatuses[order.name];
return (
<TableRow key={order.name}>
<TableCell className="font-medium">{order.name}</TableCell>
<TableCell>{order.customer_name}</TableCell>
<TableCell>{order.grand_total?.toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}</TableCell>
<TableCell>
{!projectStatus && <Badge variant="outline">Venda</Badge>}
{projectStatus?.stage === "pre_projeto" && <Badge className="bg-purple-500">Pré-Projeto</Badge>}
{projectStatus?.stage === "backlog_tecnico" && <Badge className="bg-yellow-500">Backlog Técnico</Badge>}
{projectStatus?.stage === "planejamento" && <Badge className="bg-orange-500">Planejamento</Badge>}
{projectStatus?.stage === "em_execucao" && <Badge className="bg-green-500">Em Execução</Badge>}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="w-16 h-2 bg-gray-200 rounded-full overflow-hidden">
<div className="h-full bg-green-500" style={{ width: `${order.per_delivered || 0}%` }} />
</div>
<span className="text-xs">{order.per_delivered || 0}%</span>
</div>
</TableCell>
<TableCell>{getStatusBadge(order.status)}</TableCell>
<TableCell className="text-right">
<Button
variant="ghost"
size="icon"
onClick={() => { setSelectedOrder(order); setShowProjectDialog(true); }}
data-testid={`btn-manage-project-${order.name}`}
>
<ArrowRight className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
);
})}
{salesOrders.length === 0 && (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
Nenhum pedido encontrado. Sincronize com o Arcádia SOE.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="services" className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Catálogo de Serviços - Engenharia Ambiental</h2>
<Button data-testid="btn-new-service" onClick={openNewService}>
<Plus className="h-4 w-4 mr-2" /> Novo Serviço
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{allServices.map((service) => (
<Card key={service.id} className="hover:shadow-md transition-shadow">
<CardHeader>
<div className="flex items-center justify-between">
<Badge variant="outline">{service.category}</Badge>
{service.basePrice && (
<span className="text-lg font-bold text-green-600">
{Number(service.basePrice).toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}
</span>
)}
</div>
<CardTitle className="text-base">{service.name}</CardTitle>
<CardDescription>{service.description}</CardDescription>
</CardHeader>
<CardContent>
{service.items && service.items.length > 0 && (
<>
<p className="text-xs text-muted-foreground mb-2">Itens inclusos:</p>
<ul className="text-xs space-y-1">
{service.items.map((item, i) => (
<li key={i} className="flex items-center gap-1">
<CheckCircle className="h-3 w-3 text-green-500" />
{item}
</li>
))}
</ul>
</>
)}
<div className="flex gap-2 mt-4">
{service.id > 0 && (
<Button variant="outline" size="sm" onClick={() => openEditService(service)}>
<Edit className="h-3 w-3 mr-1" /> Editar
</Button>
)}
<Button size="sm" onClick={() => { setSelectedService(service); setShowProposalDialog(true); }}>
Criar Proposta
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
<TabsContent value="pipeline" className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Pipeline de Vendas</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card className="border-t-4 border-t-gray-400">
<CardHeader className="pb-2">
<CardTitle className="text-sm">Prospecção</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0</div>
<p className="text-xs text-muted-foreground">R$ 0,00</p>
</CardContent>
</Card>
<Card className="border-t-4 border-t-blue-400">
<CardHeader className="pb-2">
<CardTitle className="text-sm">Proposta Enviada</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{proposals.filter((p: any) => p.status === "enviada").length}</div>
<p className="text-xs text-muted-foreground">Em negociação</p>
</CardContent>
</Card>
<Card className="border-t-4 border-t-yellow-400">
<CardHeader className="pb-2">
<CardTitle className="text-sm">Em Execução</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{pendingOrders}</div>
<p className="text-xs text-muted-foreground">
{salesOrders.filter(o => o.status !== "Completed").reduce((s, o) => s + o.grand_total, 0).toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}
</p>
</CardContent>
</Card>
<Card className="border-t-4 border-t-green-400">
<CardHeader className="pb-2">
<CardTitle className="text-sm">Concluído</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{completedOrders}</div>
<p className="text-xs text-muted-foreground">
{salesOrders.filter(o => o.status === "Completed").reduce((s, o) => s + o.grand_total, 0).toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}
</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle>Fluxo: Proposta Projeto</CardTitle>
<CardDescription>Workflow de conversão comercial para Engenharia Ambiental</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-blue-100 flex items-center justify-center">
<FileText className="h-8 w-8 text-blue-600" />
</div>
<span className="text-sm mt-2">Proposta</span>
</div>
<ArrowRight className="h-6 w-6 text-gray-400" />
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-yellow-100 flex items-center justify-center">
<Send className="h-8 w-8 text-yellow-600" />
</div>
<span className="text-sm mt-2">Negociação</span>
</div>
<ArrowRight className="h-6 w-6 text-gray-400" />
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-purple-100 flex items-center justify-center">
<CheckCircle className="h-8 w-8 text-purple-600" />
</div>
<span className="text-sm mt-2">Aprovação</span>
</div>
<ArrowRight className="h-6 w-6 text-gray-400" />
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-orange-100 flex items-center justify-center">
<DollarSign className="h-8 w-8 text-orange-600" />
</div>
<span className="text-sm mt-2">Pedido</span>
</div>
<ArrowRight className="h-6 w-6 text-gray-400" />
<div className="flex flex-col items-center">
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
<Rocket className="h-8 w-8 text-green-600" />
</div>
<span className="text-sm mt-2">Projeto</span>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
<Dialog open={showProposalDialog} onOpenChange={setShowProposalDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Nova Proposta Comercial</DialogTitle>
</DialogHeader>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
createProposalMutation.mutate({
title: formData.get("title") as string,
opportunityId: null,
status: "rascunho",
validUntil: formData.get("validUntil") as string,
totalValue: selectedService?.basePrice || Number(formData.get("totalValue")) || 0,
notes: formData.get("notes") as string,
});
}}>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="customer">Cliente</Label>
<Select name="customer" defaultValue={selectedCustomer}>
<SelectTrigger><SelectValue placeholder="Selecione o cliente" /></SelectTrigger>
<SelectContent>
{erpCustomers.map((c) => (
<SelectItem key={c.name} value={c.name}>{c.customer_name || c.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="service">Serviço</Label>
<Select name="service" defaultValue={selectedService?.id?.toString()} onValueChange={(v) => {
const t = allServices.find(s => s.id.toString() === v);
setSelectedService(t || null);
}}>
<SelectTrigger><SelectValue placeholder="Selecione o serviço" /></SelectTrigger>
<SelectContent>
{allServices.map((s) => (
<SelectItem key={s.id} value={s.id.toString()}>{s.name}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="title">Título da Proposta *</Label>
<Input
id="title"
name="title"
placeholder="Ex: Monitoramento Semestral - Cliente X"
defaultValue={selectedService ? `${selectedService.name} - ${selectedCustomer}` : ""}
required
/>
</div>
{selectedService && (
<Card className="bg-muted/50">
<CardHeader className="pb-2">
<CardTitle className="text-sm">{selectedService.name}</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-2">{selectedService.description}</p>
{selectedService.items && selectedService.items.length > 0 && (
<div className="flex flex-wrap gap-1">
{selectedService.items.map((item, i) => (
<Badge key={i} variant="secondary" className="text-xs">{item}</Badge>
))}
</div>
)}
{selectedService.basePrice && (
<p className="mt-3 text-lg font-bold text-green-600">
Valor base: {Number(selectedService.basePrice).toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}
</p>
)}
</CardContent>
</Card>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="totalValue">Valor Total (R$)</Label>
<Input
id="totalValue"
name="totalValue"
type="number"
defaultValue={selectedService?.basePrice || ""}
/>
</div>
<div className="space-y-2">
<Label htmlFor="validUntil">Válida até</Label>
<Input id="validUntil" name="validUntil" type="date" />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="notes">Observações</Label>
<Textarea id="notes" name="notes" placeholder="Condições especiais, escopo adicional..." />
</div>
</div>
<DialogFooter className="mt-6">
<Button type="button" variant="outline" onClick={() => setShowProposalDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={createProposalMutation.isPending}>
{createProposalMutation.isPending ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
Criar Proposta
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showServiceDialog} onOpenChange={setShowServiceDialog}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>{editingService ? "Editar Serviço" : "Novo Serviço"}</DialogTitle>
</DialogHeader>
<form onSubmit={handleSaveService}>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="code">Código</Label>
<Input id="code" name="code" placeholder="Ex: MON-001" defaultValue={editingService?.code || ""} />
</div>
<div className="space-y-2">
<Label htmlFor="category">Categoria</Label>
<Select name="category" defaultValue={editingService?.category || "Monitoramento"}>
<SelectTrigger><SelectValue placeholder="Selecione" /></SelectTrigger>
<SelectContent>
{serviceCategories.map((cat) => (
<SelectItem key={cat} value={cat}>{cat}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="name">Nome do Serviço *</Label>
<Input id="name" name="name" placeholder="Ex: Monitoramento de Águas Subterrâneas" defaultValue={editingService?.name || ""} required />
</div>
<div className="space-y-2">
<Label htmlFor="description">Descrição</Label>
<Textarea id="description" name="description" placeholder="Descrição do serviço..." defaultValue={editingService?.description || ""} />
</div>
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="basePrice">Valor Base (R$)</Label>
<Input id="basePrice" name="basePrice" type="number" step="0.01" defaultValue={editingService?.basePrice || ""} />
</div>
<div className="space-y-2">
<Label htmlFor="unit">Unidade</Label>
<Input id="unit" name="unit" placeholder="Ex: campanha" defaultValue={editingService?.unit || ""} />
</div>
<div className="space-y-2">
<Label htmlFor="estimatedDuration">Duração (dias)</Label>
<Input id="estimatedDuration" name="estimatedDuration" type="number" defaultValue={editingService?.estimatedDuration || ""} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="items">Itens Inclusos (um por linha)</Label>
<Textarea
id="items"
value={newItems}
onChange={(e) => setNewItems(e.target.value)}
placeholder={"Mobilização de equipe\nColeta de amostras\nAnálises laboratoriais\nRelatório técnico"}
rows={4}
/>
</div>
</div>
<DialogFooter className="mt-6">
<Button type="button" variant="outline" onClick={() => setShowServiceDialog(false)}>Cancelar</Button>
<Button type="submit" disabled={createServiceMutation.isPending || updateServiceMutation.isPending}>
{(createServiceMutation.isPending || updateServiceMutation.isPending) && <Loader2 className="h-4 w-4 animate-spin mr-2" />}
{editingService ? "Salvar" : "Criar Serviço"}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
<Dialog open={showProjectDialog} onOpenChange={setShowProjectDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Rocket className="h-5 w-5" />
Gerenciar Projeto - {selectedOrder?.name}
</DialogTitle>
</DialogHeader>
{selectedOrder && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4 p-4 bg-muted/30 rounded-lg">
<div>
<span className="text-sm text-muted-foreground">Cliente</span>
<p className="font-medium">{selectedOrder.customer_name}</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Valor Total</span>
<p className="font-medium text-green-600">{selectedOrder.grand_total?.toLocaleString("pt-BR", { style: "currency", currency: "BRL" })}</p>
</div>
</div>
<div className="space-y-2">
<Label>Fluxo do Projeto</Label>
<div className="flex items-center gap-2 flex-wrap">
{["venda", "pre_projeto", "backlog_tecnico", "planejamento", "em_execucao"].map((stage, idx) => {
const currentStage = projectStatuses[selectedOrder.name]?.stage || "venda";
const stages = ["venda", "pre_projeto", "backlog_tecnico", "planejamento", "em_execucao"];
const currentIdx = stages.indexOf(currentStage);
const isActive = idx <= currentIdx;
const labels: Record<string, string> = {
venda: "Venda",
pre_projeto: "Pré-Projeto",
backlog_tecnico: "Backlog Técnico",
planejamento: "Planejamento",
em_execucao: "Em Execução"
};
return (
<div key={stage} className="flex items-center">
<div className={`px-3 py-1 rounded-full text-xs font-medium ${isActive ? 'bg-green-500 text-white' : 'bg-gray-200 text-gray-600'}`}>
{labels[stage]}
</div>
{idx < 4 && <ArrowRight className="h-4 w-4 text-muted-foreground mx-1" />}
</div>
);
})}
</div>
</div>
<div className="border-t pt-4 space-y-4">
<div className="space-y-2">
<Label>Avançar para</Label>
<Select
value={projectStatuses[selectedOrder.name]?.stage || "venda"}
onValueChange={(value) => {
setProjectStatuses({
...projectStatuses,
[selectedOrder.name]: {
...projectStatuses[selectedOrder.name],
orderId: selectedOrder.name,
stage: value as any
}
});
}}
>
<SelectTrigger data-testid="select-project-stage">
<SelectValue placeholder="Selecione a etapa" />
</SelectTrigger>
<SelectContent>
<SelectItem value="venda">Venda (aguardando aprovação)</SelectItem>
<SelectItem value="pre_projeto">Pré-Projeto (aprovado, aguardando backlog)</SelectItem>
<SelectItem value="backlog_tecnico">Backlog Técnico (área técnica)</SelectItem>
<SelectItem value="planejamento">Planejamento (definindo execução)</SelectItem>
<SelectItem value="em_execucao">Em Execução</SelectItem>
</SelectContent>
</Select>
</div>
{(projectStatuses[selectedOrder.name]?.stage === "backlog_tecnico" ||
projectStatuses[selectedOrder.name]?.stage === "planejamento" ||
projectStatuses[selectedOrder.name]?.stage === "em_execucao") && (
<>
<div className="space-y-2">
<Label htmlFor="technicalTeam">Equipe Técnica Responsável</Label>
<Select name="technicalTeam">
<SelectTrigger>
<SelectValue placeholder="Selecionar equipe" />
</SelectTrigger>
<SelectContent>
<SelectItem value="alfa">Equipe Alfa</SelectItem>
<SelectItem value="beta">Equipe Beta</SelectItem>
<SelectItem value="gamma">Equipe Gamma</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="responsiblePerson">Responsável Técnico</Label>
<Select name="responsiblePerson">
<SelectTrigger>
<SelectValue placeholder="Selecionar responsável" />
</SelectTrigger>
<SelectContent>
<SelectItem value="maria">Maria Santos - Eng. Ambiental</SelectItem>
<SelectItem value="carlos">Carlos Oliveira - Geólogo</SelectItem>
<SelectItem value="fernanda">Fernanda Souza - Hidrogeóloga</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
{(projectStatuses[selectedOrder.name]?.stage === "planejamento" ||
projectStatuses[selectedOrder.name]?.stage === "em_execucao") && (
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="plannedStart">Data Início Planejada</Label>
<Input id="plannedStart" name="plannedStart" type="date" />
</div>
<div className="space-y-2">
<Label htmlFor="plannedEnd">Data Fim Planejada</Label>
<Input id="plannedEnd" name="plannedEnd" type="date" />
</div>
</div>
)}
<div className="space-y-2">
<Label htmlFor="projectNotes">Observações</Label>
<Textarea id="projectNotes" name="projectNotes" placeholder="Notas sobre o projeto..." rows={2} />
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowProjectDialog(false)}>Fechar</Button>
<Button onClick={() => {
toast({ title: "Projeto atualizado", description: `${selectedOrder.name} movido para ${projectStatuses[selectedOrder.name]?.stage || "venda"}` });
setShowProjectDialog(false);
}} data-testid="btn-save-project">
Salvar Alterações
</Button>
</DialogFooter>
</div>
)}
</DialogContent>
</Dialog>
</div>
</BrowserFrame>
);
}