🚀 Initial commit: Versão atual do TrackSteel APP

This commit is contained in:
2026-03-18 21:17:53 +00:00
commit bde410c9ad
633 changed files with 108150 additions and 0 deletions
+207
View File
@@ -0,0 +1,207 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Shield, Key, Code, Database, Settings, HardDrive, Play } from 'lucide-react';
import { ApiKeysManager } from '@/components/admin/ApiKeysManager';
import { JsonCodesManager } from '@/components/admin/JsonCodesManager';
import { FunctionsManager } from '@/components/users/FunctionsManager';
import { PrivilegesManager } from '@/components/users/PrivilegesManager';
import BackupManager from '@/components/admin/BackupManager';
import { ApontamentoMassa } from '@/components/admin/ApontamentoMassa';
import { useUserManagement } from '@/hooks/useUserManagement';
const Admin = () => {
const {
functions,
privileges,
createFunction,
updateFunction,
deleteFunction,
createPrivilege: createPrivilegeBase,
updatePrivilege: updatePrivilegeBase,
deletePrivilege
} = useUserManagement();
// Wrapper functions to match expected signatures
const createPrivilege = async (data: { name: string; description?: string; permissions: Record<string, boolean> }): Promise<void> => {
await createPrivilegeBase(data);
};
const updatePrivilege = async (id: string, data: { name: string; description?: string; permissions: Record<string, boolean> }): Promise<void> => {
await updatePrivilegeBase(id, data);
};
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div className="flex flex-col gap-2">
<h1 className="text-2xl md:text-3xl font-bold text-foreground">Administração</h1>
<p className="text-sm md:text-base text-muted-foreground">Configurações avançadas do sistema</p>
</div>
<Tabs defaultValue="api-keys" className="w-full">
<TabsList className="grid w-full grid-cols-3 md:grid-cols-7 bg-muted h-auto p-1">
<TabsTrigger
value="api-keys"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Key className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">API Keys</span>
<span className="sm:hidden">Keys</span>
</TabsTrigger>
<TabsTrigger
value="json-codes"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Code className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">JSON Codes</span>
<span className="sm:hidden">JSON</span>
</TabsTrigger>
<TabsTrigger
value="functions"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Settings className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Funções</span>
<span className="sm:hidden">Func</span>
</TabsTrigger>
<TabsTrigger
value="privileges"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Shield className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Privilégios</span>
<span className="sm:hidden">Priv</span>
</TabsTrigger>
<TabsTrigger
value="apontamento-massa"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Play className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Apontamento Massa</span>
<span className="sm:hidden">Massa</span>
</TabsTrigger>
<TabsTrigger
value="backup"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<HardDrive className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Backup</span>
<span className="sm:hidden">Backup</span>
</TabsTrigger>
<TabsTrigger
value="database"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Database className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Database</span>
<span className="sm:hidden">DB</span>
</TabsTrigger>
</TabsList>
<TabsContent value="api-keys" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Key className="h-4 w-4 md:h-5 md:w-5" />
Gerenciar API Keys
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<ApiKeysManager />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="json-codes" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Code className="h-4 w-4 md:h-5 md:w-5" />
Gerenciar JSON Codes
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<JsonCodesManager />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="functions" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Settings className="h-4 w-4 md:h-5 md:w-5" />
Gerenciar Funções
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<FunctionsManager
functions={functions}
onCreate={createFunction}
onUpdate={updateFunction}
onDelete={deleteFunction}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="privileges" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Shield className="h-4 w-4 md:h-5 md:w-5" />
Gerenciar Privilégios
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<PrivilegesManager
privileges={privileges}
onCreate={createPrivilege}
onUpdate={updatePrivilege}
onDelete={deletePrivilege}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="apontamento-massa" className="space-y-4 mt-4">
<ApontamentoMassa />
</TabsContent>
<TabsContent value="backup" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<HardDrive className="h-4 w-4 md:h-5 md:w-5" />
Gerenciar Backup
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<BackupManager />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="database" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Database className="h-4 w-4 md:h-5 md:w-5" />
Configurações do Database
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<div className="text-center py-6 md:py-8 text-muted-foreground">
<Database className="mx-auto h-8 w-8 md:h-12 md:w-12 mb-4 opacity-50" />
<p className="text-sm md:text-base">Configurações do database estarão disponíveis em breve</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export default Admin;
+140
View File
@@ -0,0 +1,140 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ClipboardList, BarChart3, Settings } from 'lucide-react';
import { ApontamentoForm } from '@/components/apontamento/ApontamentoForm';
import { ApontamentosListOtimizado } from '@/components/apontamento/ApontamentosListOtimizado';
import { ProcessosList } from '@/components/apontamento/ProcessosList';
import { useApontamentosProducao } from '@/hooks/useApontamentosProducao';
import { useIsMobile } from '@/hooks/use-mobile';
import { usePermissionControl } from '@/hooks/usePermissionControl';
const ApontamentoProducao = () => {
const [activeTab, setActiveTab] = useState('apontamento');
const { apontamentos, loading } = useApontamentosProducao();
const isMobile = useIsMobile();
const { canCreate, canEdit } = usePermissionControl();
return (
<div className="min-h-screen bg-background p-2 sm:p-4 md:p-6">
<div className="max-w-7xl mx-auto space-y-4 sm:space-y-6">
{/* Header */}
<div className="space-y-2">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-foreground">
Apontamento de Produção
</h1>
<p className="text-sm sm:text-base text-muted-foreground">
Controle diário de produção por processo com cache inteligente
</p>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className={`w-full bg-muted border-border ${
isMobile
? 'grid grid-cols-1 gap-1 h-auto p-1'
: 'grid grid-cols-3 h-10'
}`}>
<TabsTrigger
value="apontamento"
className={`flex items-center gap-2 text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground ${
isMobile ? 'w-full justify-start p-3 text-sm' : ''
}`}
>
<ClipboardList className="h-4 w-4" />
Novo Apontamento
</TabsTrigger>
<TabsTrigger
value="historico"
className={`flex items-center gap-2 text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground ${
isMobile ? 'w-full justify-start p-3 text-sm' : ''
}`}
>
<BarChart3 className="h-4 w-4" />
Histórico
</TabsTrigger>
<TabsTrigger
value="processos"
className={`flex items-center gap-2 text-muted-foreground data-[state=active]:bg-background data-[state=active]:text-foreground ${
isMobile ? 'w-full justify-start p-3 text-sm' : ''
}`}
>
<Settings className="h-4 w-4" />
Processos
</TabsTrigger>
</TabsList>
<TabsContent value="apontamento" className="space-y-4 sm:space-y-6 mt-4">
{canCreate() ? (
<Card className="bg-card border-border">
<CardHeader className="pb-4">
<CardTitle className="text-card-foreground flex items-center gap-2 text-lg sm:text-xl">
<ClipboardList className="h-5 w-5" />
Novo Apontamento de Produção
</CardTitle>
</CardHeader>
<CardContent>
<ApontamentoForm />
</CardContent>
</Card>
) : (
<Card className="bg-card border-border">
<CardContent className="p-8 text-center">
<p className="text-muted-foreground">
Você não tem permissão para criar apontamentos de produção.
</p>
</CardContent>
</Card>
)}
</TabsContent>
<TabsContent value="historico" className="space-y-4 sm:space-y-6 mt-4">
<Card className="bg-card border-border">
<CardHeader className="pb-4">
<CardTitle className="text-card-foreground flex items-center justify-between flex-wrap gap-2">
<div className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
<span className="text-lg sm:text-xl">Histórico de Apontamentos</span>
</div>
<Badge variant="secondary" className="bg-secondary text-secondary-foreground border-border">
{apontamentos.length} apontamentos
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
<ApontamentosListOtimizado />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="processos" className="space-y-4 sm:space-y-6 mt-4">
{canEdit() ? (
<Card className="bg-card border-border">
<CardHeader className="pb-4">
<CardTitle className="text-card-foreground flex items-center gap-2 text-lg sm:text-xl">
<Settings className="h-5 w-5" />
Gerenciar Processos
</CardTitle>
</CardHeader>
<CardContent>
<ProcessosList />
</CardContent>
</Card>
) : (
<Card className="bg-card border-border">
<CardContent className="p-8 text-center">
<p className="text-muted-foreground">
Você não tem permissão para gerenciar processos.
</p>
</CardContent>
</Card>
)}
</TabsContent>
</Tabs>
</div>
</div>
);
};
export default ApontamentoProducao;
+47
View File
@@ -0,0 +1,47 @@
import React from 'react';
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AtribuicoesForm } from '@/components/atribuicoes/AtribuicoesForm';
import { AtribuicoesTable } from '@/components/atribuicoes/AtribuicoesTable';
import { useAtribuicoes } from '@/hooks/useAtribuicoes';
import { useMobileResponsive } from '@/hooks/useMobileResponsive';
function Atribuicoes() {
const { canManage } = useAtribuicoes();
const { isMobile } = useMobileResponsive();
return (
<StandardPageLayout
title="Atribuições"
subtitle={canManage ? "Gerencie as atribuições de todos os usuários" : "Visualize suas atribuições"}
>
<div className="space-y-6">
<Tabs defaultValue={canManage ? "cadastro" : "tabela"} className="w-full">
<TabsList className={`grid w-full ${canManage ? 'grid-cols-2' : 'grid-cols-1'}`}>
{canManage && (
<TabsTrigger value="cadastro" className={isMobile ? 'text-sm' : ''}>
Cadastro
</TabsTrigger>
)}
<TabsTrigger value="tabela" className={isMobile ? 'text-sm' : ''}>
{canManage ? 'Tabela Geral' : 'Minhas Atribuições'}
</TabsTrigger>
</TabsList>
{canManage && (
<TabsContent value="cadastro" className="space-y-6">
<AtribuicoesForm />
</TabsContent>
)}
<TabsContent value="tabela" className="space-y-6">
<AtribuicoesTable />
</TabsContent>
</Tabs>
</div>
</StandardPageLayout>
);
}
export default Atribuicoes;
+484
View File
@@ -0,0 +1,484 @@
import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { useBrandSettings } from '@/hooks/useBrandSettings';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ThemeToggle } from '@/components/ThemeToggle';
import { BeamsBackground } from '@/components/ui/beams-background';
import { ForgotPasswordModal } from '@/components/auth/ForgotPasswordModal';
import { PasswordResetForm } from '@/components/auth/PasswordResetForm';
import { toast } from 'sonner';
import { Eye, EyeOff, Mail, Lock } from 'lucide-react';
const Auth = () => {
const [isLoading, setIsLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const [showForgotPassword, setShowForgotPassword] = useState(false);
const [loginData, setLoginData] = useState({ email: '', password: '' });
const [signupData, setSignupData] = useState({ email: '', password: '', confirmPassword: '' });
const { signIn, signUp, user, loading, isRecoveryFlow, session } = useAuth();
const { brandSettings } = useBrandSettings();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const location = useLocation();
// Estado para controlar se está em modo de recuperação
const [showPasswordReset, setShowPasswordReset] = useState(false);
useEffect(() => {
console.log('🔍 Auth.tsx - Verificando fluxo de recuperação...');
console.log('Location:', {
pathname: location.pathname,
search: location.search,
hash: location.hash
});
console.log('Auth states:', {
user: !!user,
session: !!session,
isRecoveryFlow,
loading
});
// Verificar se é fluxo de recuperação
const recoveryType = searchParams.get('type');
const hashParams = new URLSearchParams(location.hash.substring(1));
const hashType = hashParams.get('type');
const accessToken = hashParams.get('access_token');
const refreshToken = hashParams.get('refresh_token');
console.log('🔍 Parâmetros de recuperação:', {
recoveryType,
hashType,
accessToken: !!accessToken,
refreshToken: !!refreshToken,
isRecoveryFlow
});
// Se há type=recovery OU isRecoveryFlow OU tokens, mostrar tela de reset
const shouldShowReset = recoveryType === 'recovery' ||
hashType === 'recovery' ||
isRecoveryFlow ||
(accessToken && refreshToken);
console.log('🔑 Deve mostrar reset de senha:', shouldShowReset);
setShowPasswordReset(Boolean(shouldShowReset));
}, [searchParams, location.hash, location.search, isRecoveryFlow, user, session]);
// Redirecionar usuários autenticados para página principal (exceto em fluxo de recuperação)
useEffect(() => {
if (!loading && user && !showPasswordReset) {
console.log('🔄 Redirecionando usuário autenticado para página principal');
navigate('/');
}
}, [user, loading, navigate, showPasswordReset]);
// Mostrar formulário de redefinição de senha se em modo de recuperação
if (!loading && showPasswordReset) {
console.log('🔐 Exibindo formulário de redefinição de senha');
return (
<BeamsBackground intensity="medium">
<div className="min-h-screen flex items-center justify-center p-4 transition-colors duration-300">
<div className="absolute top-4 right-4">
<ThemeToggle />
</div>
<div className="w-full max-w-md">
<div className="text-center mb-8">
{brandSettings?.logo_url && (
<div className="flex justify-center mb-4">
<img
src={brandSettings.logo_url}
alt="Logo da empresa"
className="h-16 w-auto object-contain"
/>
</div>
)}
<h1 className="text-3xl font-bold text-foreground mb-2">
{brandSettings?.company_name || 'BSystem'}
</h1>
<p className="text-sm text-muted-foreground mb-4">
Defina sua nova senha
</p>
</div>
<PasswordResetForm />
<div className="text-center mt-6">
<button
onClick={() => {
setShowPasswordReset(false);
window.history.replaceState({}, '', '/auth');
}}
className="text-sm text-muted-foreground hover:text-foreground underline"
>
Voltar ao login
</button>
</div>
<p className="text-center text-sm text-muted-foreground mt-4 italic">
Desenvolvido por TrackSteel
</p>
</div>
</div>
</BeamsBackground>
);
}
// Mostrar estado de carregamento
if (loading) {
console.log('⏳ Exibindo estado de carregamento');
return (
<BeamsBackground intensity="medium">
<div className="min-h-screen flex items-center justify-center p-4">
<div className="text-center">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-primary mx-auto"></div>
<p className="mt-4 text-muted-foreground">Carregando...</p>
</div>
</div>
</BeamsBackground>
);
}
const validatePassword = (password: string) => {
const minLength = password.length >= 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
const score = [minLength, hasUpperCase, hasLowerCase, hasNumbers, hasSpecialChar].filter(Boolean).length;
return {
score,
minLength,
hasUpperCase,
hasLowerCase,
hasNumbers,
hasSpecialChar,
isValid: score >= 4 && minLength
};
};
const passwordStrength = validatePassword(signupData.password);
// Email validation
const isValidEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!loginData.email || !loginData.password) {
toast.error('Por favor, preencha todos os campos');
return;
}
if (!isValidEmail(loginData.email)) {
toast.error('Por favor, insira um email válido');
return;
}
setIsLoading(true);
const { error } = await signIn(loginData.email, loginData.password);
if (error) {
if (error.message.includes('Invalid login credentials')) {
toast.error('Email ou senha incorretos');
} else {
toast.error('Erro ao fazer login: ' + error.message);
}
} else {
toast.success('Login realizado com sucesso!');
navigate('/');
}
setIsLoading(false);
};
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
if (!signupData.email || !signupData.password || !signupData.confirmPassword) {
toast.error('Por favor, preencha todos os campos');
return;
}
if (!isValidEmail(signupData.email)) {
toast.error('Por favor, insira um email válido');
return;
}
if (!passwordStrength.isValid) {
toast.error('A senha deve ter pelo menos 8 caracteres e incluir maiúsculas, minúsculas, números e símbolos');
return;
}
if (signupData.password !== signupData.confirmPassword) {
toast.error('As senhas não coincidem');
return;
}
setIsLoading(true);
const { error } = await signUp(signupData.email, signupData.password);
if (error) {
if (error.message.includes('User already registered')) {
toast.error('Este email já está cadastrado');
} else {
toast.error('Erro ao criar conta: ' + error.message);
}
} else {
toast.success('Conta criada com sucesso! Verifique seu email.');
}
setIsLoading(false);
};
const getPasswordStrengthColor = (score: number) => {
if (score <= 2) return 'bg-red-500';
if (score <= 3) return 'bg-yellow-500';
return 'bg-green-500';
};
const getPasswordStrengthText = (score: number) => {
if (score <= 2) return 'Fraca';
if (score <= 3) return 'Média';
return 'Forte';
};
console.log('📱 Renderizando página de login/cadastro');
return (
<BeamsBackground intensity="medium">
<div className="min-h-screen flex items-center justify-center p-4 transition-colors duration-300">
{/* Theme Toggle Button */}
<div className="absolute top-4 right-4">
<ThemeToggle />
</div>
<div className="w-full max-w-md">
{/* Logo and Company Name */}
<div className="text-center mb-8">
{brandSettings?.logo_url && (
<div className="flex justify-center mb-4">
<img
src={brandSettings.logo_url}
alt="Logo da empresa"
className="h-16 w-auto object-contain"
/>
</div>
)}
<h1 className="text-3xl font-bold text-foreground mb-2">
Bem-vindo ao {brandSettings?.company_name || 'BSystem'}
</h1>
<p className="text-muted-foreground">Entre em sua conta ou crie uma nova</p>
</div>
<Card className="shadow-xl border-0 bg-background/80 backdrop-blur-sm">
<CardHeader className="space-y-1 pb-4">
<div className="flex justify-center mb-4">
<div className="w-12 h-12 bg-gradient-to-r from-blue-500 to-purple-600 rounded-full flex items-center justify-center">
<Lock className="w-6 h-6 text-white" />
</div>
</div>
</CardHeader>
<CardContent>
<Tabs defaultValue="login" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6">
<TabsTrigger value="login" className="text-sm">Entrar</TabsTrigger>
<TabsTrigger value="signup" className="text-sm">Cadastrar</TabsTrigger>
</TabsList>
<TabsContent value="login" className="space-y-4">
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="login-email" className="text-sm font-medium">
Email
</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="login-email"
type="email"
placeholder="seu@email.com"
value={loginData.email}
onChange={(e) => setLoginData({ ...loginData, email: e.target.value })}
className="pl-10 h-12"
disabled={isLoading}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="login-password" className="text-sm font-medium">
Senha
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="login-password"
type={showPassword ? "text" : "password"}
placeholder="Sua senha"
value={loginData.password}
onChange={(e) => setLoginData({ ...loginData, password: e.target.value })}
className="pl-10 pr-10 h-12"
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
</div>
<Button
type="submit"
className="w-full h-12 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium text-sm"
disabled={isLoading}
>
{isLoading ? 'Entrando...' : 'Entrar'}
</Button>
{/* Botão Esqueci a Senha */}
<div className="text-center">
<button
type="button"
onClick={() => setShowForgotPassword(true)}
className="text-sm text-muted-foreground hover:text-foreground underline"
disabled={isLoading}
>
Esqueci a senha
</button>
</div>
</form>
</TabsContent>
<TabsContent value="signup" className="space-y-4">
<form onSubmit={handleSignup} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="signup-email" className="text-sm font-medium">
Email
</Label>
<div className="relative">
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="signup-email"
type="email"
placeholder="seu@email.com"
value={signupData.email}
onChange={(e) => setSignupData({ ...signupData, email: e.target.value })}
className="pl-10 h-12"
disabled={isLoading}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="signup-password" className="text-sm font-medium">
Senha
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="signup-password"
type={showPassword ? "text" : "password"}
placeholder="Mínimo 8 caracteres"
value={signupData.password}
onChange={(e) => setSignupData({ ...signupData, password: e.target.value })}
className="pl-10 pr-10 h-12"
disabled={isLoading}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-3 text-muted-foreground hover:text-foreground"
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
{/* Password Strength Indicator */}
{signupData.password && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<div className="flex-1 bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full transition-all duration-300 ${getPasswordStrengthColor(passwordStrength.score)}`}
style={{ width: `${(passwordStrength.score / 5) * 100}%` }}
></div>
</div>
<span className="text-xs text-muted-foreground">
{getPasswordStrengthText(passwordStrength.score)}
</span>
</div>
<div className="text-xs space-y-1">
<div className={`flex items-center gap-1 ${passwordStrength.minLength ? 'text-green-600' : 'text-red-600'}`}>
<span className="w-2 h-2 rounded-full bg-current"></span>
Mínimo 8 caracteres
</div>
<div className={`flex items-center gap-1 ${passwordStrength.hasUpperCase ? 'text-green-600' : 'text-red-600'}`}>
<span className="w-2 h-2 rounded-full bg-current"></span>
Letra maiúscula
</div>
<div className={`flex items-center gap-1 ${passwordStrength.hasNumbers ? 'text-green-600' : 'text-red-600'}`}>
<span className="w-2 h-2 rounded-full bg-current"></span>
Número
</div>
<div className={`flex items-center gap-1 ${passwordStrength.hasSpecialChar ? 'text-green-600' : 'text-red-600'}`}>
<span className="w-2 h-2 rounded-full bg-current"></span>
Símbolo especial
</div>
</div>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="signup-confirm" className="text-sm font-medium">
Confirmar Senha
</Label>
<div className="relative">
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
id="signup-confirm"
type={showPassword ? "text" : "password"}
placeholder="Confirme sua senha"
value={signupData.confirmPassword}
onChange={(e) => setSignupData({ ...signupData, confirmPassword: e.target.value })}
className="pl-10 h-12"
disabled={isLoading}
/>
</div>
</div>
<Button
type="submit"
className="w-full h-12 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 text-white font-medium text-sm"
disabled={isLoading}
>
{isLoading ? 'Criando conta...' : 'Criar conta'}
</Button>
</form>
</TabsContent>
</Tabs>
</CardContent>
</Card>
<p className="text-center text-sm text-muted-foreground mt-6 italic">
Desenvolvido por TrackSteel
</p>
</div>
</div>
{/* Modal Esqueci a Senha */}
<ForgotPasswordModal
isOpen={showForgotPassword}
onClose={() => setShowForgotPassword(false)}
/>
</BeamsBackground>
);
};
export default Auth;
+48
View File
@@ -0,0 +1,48 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Wrench, Plus } from 'lucide-react';
import { useUserRole } from '@/hooks/useUserRole';
const BibliotecaFerramentas = () => {
const { isAdmin } = useUserRole();
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground">Ferramentas</h1>
<p className="text-sm md:text-base text-muted-foreground">Catálogo de ferramentas e equipamentos</p>
</div>
{isAdmin && (
<Button
className="mobile-full-width bg-green-600 hover:bg-green-700"
>
<Plus className="w-3 h-3 md:w-4 md:h-4 mr-2" />
<span className="text-sm md:text-base">Nova Ferramenta</span>
</Button>
)}
</div>
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Wrench className="w-4 h-4 md:w-5 md:h-5" />
Ferramentas Disponíveis
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<div className="text-center py-6 md:py-8 text-muted-foreground">
<Wrench className="mx-auto h-8 w-8 md:h-12 md:w-12 mb-4 opacity-50" />
<p className="text-sm md:text-base">Nenhuma ferramenta cadastrada ainda</p>
<p className="text-xs md:text-sm mt-2">As ferramentas serão listadas aqui quando adicionadas</p>
</div>
</CardContent>
</Card>
</div>
);
};
export default BibliotecaFerramentas;
+48
View File
@@ -0,0 +1,48 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { FileCheck, Plus } from 'lucide-react';
import { useUserRole } from '@/hooks/useUserRole';
const BibliotecaNormas = () => {
const { isAdmin } = useUserRole();
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground">Normas Técnicas</h1>
<p className="text-sm md:text-base text-muted-foreground">Biblioteca de normas e regulamentações técnicas</p>
</div>
{isAdmin && (
<Button
className="mobile-full-width bg-green-600 hover:bg-green-700"
>
<Plus className="w-3 h-3 md:w-4 md:h-4 mr-2" />
<span className="text-sm md:text-base">Nova Norma</span>
</Button>
)}
</div>
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<FileCheck className="w-4 h-4 md:w-5 md:h-5" />
Normas Disponíveis
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<div className="text-center py-6 md:py-8 text-muted-foreground">
<FileCheck className="mx-auto h-8 w-8 md:h-12 md:w-12 mb-4 opacity-50" />
<p className="text-sm md:text-base">Nenhuma norma cadastrada ainda</p>
<p className="text-xs md:text-sm mt-2">As normas técnicas serão listadas aqui quando adicionadas</p>
</div>
</CardContent>
</Card>
</div>
);
};
export default BibliotecaNormas;
+48
View File
@@ -0,0 +1,48 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Bookmark, Plus } from 'lucide-react';
import { useUserRole } from '@/hooks/useUserRole';
const BibliotecaReferencias = () => {
const { isAdmin } = useUserRole();
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground">Referências</h1>
<p className="text-sm md:text-base text-muted-foreground">Material de referência e documentação técnica</p>
</div>
{isAdmin && (
<Button
className="mobile-full-width bg-green-600 hover:bg-green-700"
>
<Plus className="w-3 h-3 md:w-4 md:h-4 mr-2" />
<span className="text-sm md:text-base">Nova Referência</span>
</Button>
)}
</div>
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Bookmark className="w-4 h-4 md:w-5 md:h-5" />
Referências Disponíveis
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<div className="text-center py-6 md:py-8 text-muted-foreground">
<Bookmark className="mx-auto h-8 w-8 md:h-12 md:w-12 mb-4 opacity-50" />
<p className="text-sm md:text-base">Nenhuma referência cadastrada ainda</p>
<p className="text-xs md:text-sm mt-2">As referências técnicas serão listadas aqui quando adicionadas</p>
</div>
</CardContent>
</Card>
</div>
);
};
export default BibliotecaReferencias;
+44
View File
@@ -0,0 +1,44 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { usePermissionControl } from '@/hooks/usePermissionControl';
const Cadastro = () => {
const { canView } = usePermissionControl();
// Verificar se pode acessar esta página
if (!canView()) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center space-y-4">
<div className="text-6xl">🔒</div>
<h2 className="text-2xl font-semibold text-muted-foreground">
Acesso Negado
</h2>
<p className="text-muted-foreground max-w-md">
Você não tem permissão para acessar esta página.
</p>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Cadastro</h1>
<p className="text-slate-400">Sistema de cadastros e registros</p>
</div>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white">Opções de Cadastro</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-400">Selecione uma opção de cadastro.</p>
</CardContent>
</Card>
</div>
);
};
export default Cadastro;
+849
View File
@@ -0,0 +1,849 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { toast } from 'sonner';
import { Save, FileText, User, MapPin, Settings2, Eye, Download, Search, Building2 } from 'lucide-react';
import { useSearchParams } from 'react-router-dom';
import { AlteracoesPopup } from '@/components/forms/AlteracoesPopup';
import { FichaTecnicaPreview } from '@/components/forms/FichaTecnicaPreview';
import { useThemeConfig } from '@/hooks/useThemeConfig';
import { useFichaTecnica, FichaTecnicaData } from '@/hooks/useFichaTecnica';
import { useUserManagement } from '@/hooks/useUserManagement';
interface InfoCalculoEstrutural {
e: boolean;
c: boolean;
na: boolean;
info: string;
}
interface FormData extends FichaTecnicaData {
data_inicio: string;
data_termino_prev: string;
status: string;
prioridade: string;
}
const initialFormData: FormData = {
of_number: '',
gestor: '',
projetista: '',
revisao: '0',
quantidade: 0,
data_inicio: '',
data_termino_prev: '',
status: 'aberta',
prioridade: 'media',
// Dados do Cliente
cliente: '',
cnpj: '',
ie: '',
endereco: '',
cidade: '',
estado: '',
cep: '',
contato_contrato: '',
fone_contrato: '',
cel_contrato: '',
email_contrato: '',
contato_obra: '',
fone_obra: '',
cel_obra: '',
email_obra: '',
contato_qualid: '',
fone_qualid: '',
cel_qualid: '',
email_qualid: '',
// Dados da Obra
eng_responsavel: '',
telefone_obra: '',
email_obra_responsavel: '',
endereco_obra: '',
cep_obra: '',
bairro_obra: '',
cidade_obra: '',
estado_obra: '',
observacoes_obra: '',
// Dados do Projeto
descricao_resumida: '',
endereco_projeto: '',
bairro_projeto: '',
cep_projeto: '',
cidade_projeto: '',
estado_projeto: '',
horarios_trabalho: '',
condicoes_acesso: '',
// Tipos de Projeto
tipo_estrutural: false,
tipo_residencial: false,
tipo_espacial: false,
tipo_comercial: false,
tipo_grades: false,
tipo_industrial: false,
tipo_cobertura: false,
tipo_com_montagem: false,
// Documentos Fornecidos
doc_calculo: false,
doc_projeto: false,
doc_detalhamento: false,
doc_cronograma: false,
doc_normas: false,
doc_especif_tecnicas: false,
doc_catalogo: false,
doc_fotos: false,
// Informações Técnicas
info_calculo_estrutural: { e: false, c: false, na: false, info: '' },
info_projeto_basico: { e: false, c: false, na: false, info: '' },
info_detalhamento: { e: false, c: false, na: false, info: '' },
info_materia_prima: { e: false, c: false, na: false, info: '' },
info_fabricacao: { e: false, c: false, na: false, info: '' },
info_grades_piso: { e: false, c: false, na: false, info: '' },
info_jateamento: { e: false, c: false, na: false, info: '' },
info_pintura_base: { e: false, c: false, na: false, info: '' },
info_pintura_inter: { e: false, c: false, na: false, info: '' },
info_pintura_acabamento: { e: false, c: false, na: false, info: '' },
info_galvanizacao: { e: false, c: false, na: false, info: '' },
info_embalagem: { e: false, c: false, na: false, info: '' },
info_transporte: { e: false, c: false, na: false, info: '' },
info_inspecao: { e: false, c: false, na: false, info: '' },
info_ensaios_lab: { e: false, c: false, na: false, info: '' },
info_databook: { e: false, c: false, na: false, info: '' },
info_pre_montagem: { e: false, c: false, na: false, info: '' },
info_placa_engenetal: { e: false, c: false, na: false, info: '' },
info_parafusos: { e: false, c: false, na: false, info: '' },
info_chumbadores: { e: false, c: false, na: false, info: '' },
info_stud_bolt: { e: false, c: false, na: false, info: '' },
info_fornec_telhas: { e: false, c: false, na: false, info: '' },
info_montagem_telhas: { e: false, c: false, na: false, info: '' },
info_forn_calhas: { e: false, c: false, na: false, info: '' },
info_mont_calhas: { e: false, c: false, na: false, info: '' },
info_steel_deck: { e: false, c: false, na: false, info: '' },
info_fornec_wall: { e: false, c: false, na: false, info: '' },
info_mont_wall: { e: false, c: false, na: false, info: '' },
info_outros_materiais: { e: false, c: false, na: false, info: '' },
// Validação
necessita_validacao_pos_detalh: false,
// Grades
grades_modelo: '',
grades_padrao_comercial: false,
grades_padrao_sa2: false,
grades_padrao_sa2_meio: false,
grades_padrao_sa3: false,
// Requisitos
req_ambientais_existem: false,
req_ambientais_quais: '',
req_saude_seguranca_existem: false,
req_saude_seguranca_quais: '',
// Alterações
alteracao_descritivo: '',
alteracao_motivo: '',
alteracao_impacto: '',
alteracao_custo: 0,
alteracao_cronograma: '',
alteracao_pecas_prontas: '',
alteracao_detalh_projeto: '',
// Cronograma
cronograma_semanas: {},
// Vistos
visto_gestor: '',
visto_pcp: '',
visto_eng: '',
visto_fab: '',
visto_exp: '',
visto_qual: '',
visto_colunas: { '1': false, '2': false, '3': false, '4': false },
};
const CadastroOF = () => {
const [searchParams] = useSearchParams();
const ofId = searchParams.get('id');
const isEditing = Boolean(ofId);
const { themeConfig } = useThemeConfig();
const { buscarFichaTecnica, salvarFichaTecnica, loading: fichaTecnicaLoading } = useFichaTecnica();
const { users } = useUserManagement();
const [formData, setFormData] = useState<FormData>(initialFormData);
const [isSaving, setIsSaving] = useState(false);
const [showPreview, setShowPreview] = useState(false);
const [searchOfNumber, setSearchOfNumber] = useState('');
// Apply theme config on component mount
useEffect(() => {
if (themeConfig) {
const root = document.documentElement;
const isDark = root.classList.contains('dark');
const theme = isDark ? themeConfig.dark_theme : themeConfig.light_theme;
Object.entries(theme).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value);
});
}
}, [themeConfig]);
const handleLoadOF = async () => {
if (!searchOfNumber.trim()) {
toast.error('Digite o número da OF para carregar');
return;
}
try {
const data = await buscarFichaTecnica(searchOfNumber.trim());
if (data) {
const parseJsonField = (field: any) => {
if (typeof field === 'string') {
try {
return JSON.parse(field);
} catch {
return { e: false, c: false, na: false, info: '' };
}
}
return field || { e: false, c: false, na: false, info: '' };
};
setFormData({
...initialFormData,
...data,
data_inicio: data.data_inicio || '',
data_termino_prev: data.data_termino_prev || '',
status: 'aberta',
prioridade: 'media',
quantidade: data.quantidade || 0,
info_calculo_estrutural: parseJsonField(data.info_calculo_estrutural),
info_projeto_basico: parseJsonField(data.info_projeto_basico),
info_detalhamento: parseJsonField(data.info_detalhamento),
info_materia_prima: parseJsonField(data.info_materia_prima),
info_fabricacao: parseJsonField(data.info_fabricacao),
info_grades_piso: parseJsonField(data.info_grades_piso),
info_jateamento: parseJsonField(data.info_jateamento),
info_pintura_base: parseJsonField(data.info_pintura_base),
info_pintura_inter: parseJsonField(data.info_pintura_inter),
info_pintura_acabamento: parseJsonField(data.info_pintura_acabamento),
info_galvanizacao: parseJsonField(data.info_galvanizacao),
info_embalagem: parseJsonField(data.info_embalagem),
info_transporte: parseJsonField(data.info_transporte),
info_inspecao: parseJsonField(data.info_inspecao),
info_ensaios_lab: parseJsonField(data.info_ensaios_lab),
info_databook: parseJsonField(data.info_databook),
info_pre_montagem: parseJsonField(data.info_pre_montagem),
info_placa_engenetal: parseJsonField(data.info_placa_engenetal),
info_parafusos: parseJsonField(data.info_parafusos),
info_chumbadores: parseJsonField(data.info_chumbadores),
info_stud_bolt: parseJsonField(data.info_stud_bolt),
info_fornec_telhas: parseJsonField(data.info_fornec_telhas),
info_montagem_telhas: parseJsonField(data.info_montagem_telhas),
info_forn_calhas: parseJsonField(data.info_forn_calhas),
info_mont_calhas: parseJsonField(data.info_mont_calhas),
info_steel_deck: parseJsonField(data.info_steel_deck),
info_fornec_wall: parseJsonField(data.info_fornec_wall),
info_mont_wall: parseJsonField(data.info_mont_wall),
info_outros_materiais: parseJsonField(data.info_outros_materiais),
});
toast.success('OF carregada com sucesso!');
} else {
toast.error('OF não encontrada');
}
} catch (error) {
toast.error('Erro ao carregar OF');
}
};
const handleChange = (field: keyof FormData, value: any) => {
setFormData(prev => ({
...prev,
[field]: value,
}));
};
const handleTechnicalInfoChange = (field: keyof FormData, key: keyof InfoCalculoEstrutural, value: any) => {
setFormData(prev => ({
...prev,
[field]: {
...(prev[field] as InfoCalculoEstrutural),
[key]: value,
},
}));
};
const handleSave = async () => {
if (!formData.of_number) {
toast.error('Número da OF é obrigatório');
return;
}
setIsSaving(true);
try {
const success = await salvarFichaTecnica(formData);
if (success && !isEditing) {
setFormData(initialFormData);
}
} catch (error) {
console.error('Erro ao salvar:', error);
toast.error('Erro ao salvar a OF');
} finally {
setIsSaving(false);
}
};
const renderTechnicalInfoField = (
label: string,
fieldName: keyof FormData,
info: InfoCalculoEstrutural
) => (
<div className="space-y-2">
<Label className="text-sm font-medium text-muted-foreground">{label}</Label>
<div className="flex gap-2 mb-2">
<div className="flex items-center space-x-1">
<Checkbox
checked={info.e}
onCheckedChange={(checked) =>
handleTechnicalInfoChange(fieldName, 'e', checked as boolean)
}
/>
<Label className="text-xs">E</Label>
</div>
<div className="flex items-center space-x-1">
<Checkbox
checked={info.c}
onCheckedChange={(checked) =>
handleTechnicalInfoChange(fieldName, 'c', checked as boolean)
}
/>
<Label className="text-xs">C</Label>
</div>
<div className="flex items-center space-x-1">
<Checkbox
checked={info.na}
onCheckedChange={(checked) =>
handleTechnicalInfoChange(fieldName, 'na', checked as boolean)
}
/>
<Label className="text-xs">N/A</Label>
</div>
</div>
<Textarea
value={info.info}
onChange={(e) =>
handleTechnicalInfoChange(fieldName, 'info', e.target.value)
}
placeholder="Informações adicionais"
className="min-h-[60px] text-xs bg-input border-border text-foreground"
/>
</div>
);
return (
<div className="container mx-auto p-4 space-y-4 bg-background text-foreground">
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-primary flex items-center gap-2">
<FileText className="w-6 h-6" />
Ficha Técnica da OF
</h1>
<div className="flex items-center gap-2">
<Button
onClick={handleSave}
disabled={isSaving}
className="bg-primary hover:bg-primary/90 text-primary-foreground"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? 'Salvando...' : 'Salvar'}
</Button>
<Button
variant="outline"
className="border-border"
onClick={() => setShowPreview(true)}
disabled={!formData.of_number}
>
<Eye className="w-4 h-4 mr-2" />
Preview
</Button>
</div>
</div>
{/* Seção para Carregar OF */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<Search className="w-5 h-5" />
Carregar OF Existente
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex gap-2">
<Input
placeholder="Digite o número da OF"
value={searchOfNumber}
onChange={(e) => setSearchOfNumber(e.target.value)}
className="bg-input border-border text-foreground"
/>
<Button
onClick={handleLoadOF}
disabled={fichaTecnicaLoading}
variant="outline"
className="border-border"
>
<Search className="w-4 h-4 mr-2" />
{fichaTecnicaLoading ? 'Carregando...' : 'Carregar OF'}
</Button>
</div>
</CardContent>
</Card>
<div className="space-y-4">
{/* Informações Básicas */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<FileText className="w-5 h-5" />
Informações Básicas
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div>
<Label htmlFor="of_number" className="text-sm font-medium text-muted-foreground">Número OF</Label>
<Input
id="of_number"
value={formData.of_number}
onChange={(e) => handleChange('of_number', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="gestor" className="text-sm font-medium text-muted-foreground">Gestor da OF</Label>
<Select value={formData.gestor || ''} onValueChange={(value) => handleChange('gestor', value)}>
<SelectTrigger className="mt-1 bg-input border-border text-foreground">
<SelectValue placeholder="Selecione o gestor" />
</SelectTrigger>
<SelectContent className="bg-popover border-border">
{users.filter(user => user.status === 'active').map((user) => (
<SelectItem key={user.id} value={user.full_name || user.email || ''}>
{user.full_name || user.email}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="projetista" className="text-sm font-medium text-muted-foreground">Projetista da OF</Label>
<Select value={formData.projetista || ''} onValueChange={(value) => handleChange('projetista', value)}>
<SelectTrigger className="mt-1 bg-input border-border text-foreground">
<SelectValue placeholder="Selecione o projetista" />
</SelectTrigger>
<SelectContent className="bg-popover border-border">
{users.filter(user => user.status === 'active').map((user) => (
<SelectItem key={user.id} value={user.full_name || user.email || ''}>
{user.full_name || user.email}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="revisao" className="text-sm font-medium text-muted-foreground">Revisão</Label>
<Input
id="revisao"
value={formData.revisao || '0'}
onChange={(e) => handleChange('revisao', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="data_inicio" className="text-sm font-medium text-muted-foreground">Data Início</Label>
<Input
id="data_inicio"
type="date"
value={formData.data_inicio}
onChange={(e) => handleChange('data_inicio', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="data_termino_prev" className="text-sm font-medium text-muted-foreground">Data Prazo</Label>
<Input
id="data_termino_prev"
type="date"
value={formData.data_termino_prev}
onChange={(e) => handleChange('data_termino_prev', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="quantidade" className="text-sm font-medium text-muted-foreground">Quantidade (t)</Label>
<Input
id="quantidade"
type="number"
value={formData.quantidade || ''}
onChange={(e) => handleChange('quantidade', parseFloat(e.target.value) || 0)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="descricao_resumida" className="text-sm font-medium text-muted-foreground">Descrição Resumida</Label>
<Input
id="descricao_resumida"
value={formData.descricao_resumida || ''}
onChange={(e) => handleChange('descricao_resumida', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
</div>
</CardContent>
</Card>
{/* Status e Prioridade */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<Settings2 className="w-5 h-5" />
Status
</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<div>
<Label className="text-sm font-medium text-muted-foreground">Status Atual</Label>
<Select value={formData.status} onValueChange={(value) => handleChange('status', value)}>
<SelectTrigger className="mt-1 bg-input border-border text-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-popover border-border">
<SelectItem value="aberta">Aberta</SelectItem>
<SelectItem value="em_andamento">Em Andamento</SelectItem>
<SelectItem value="concluida">Concluída</SelectItem>
<SelectItem value="arquivada">Arquivada</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm font-medium text-muted-foreground">Prioridade</Label>
<Select value={formData.prioridade} onValueChange={(value) => handleChange('prioridade', value)}>
<SelectTrigger className="mt-1 bg-input border-border text-foreground">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-popover border-border">
<SelectItem value="baixa">Baixa</SelectItem>
<SelectItem value="media">Média</SelectItem>
<SelectItem value="alta">Alta</SelectItem>
<SelectItem value="urgente">Urgente</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Informações do Cliente */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<User className="w-5 h-5" />
Informações do Cliente
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div className="md:col-span-2">
<Label htmlFor="cliente" className="text-sm font-medium text-muted-foreground">Cliente</Label>
<Input
id="cliente"
value={formData.cliente || ''}
onChange={(e) => handleChange('cliente', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="cnpj" className="text-sm font-medium text-muted-foreground">CNPJ</Label>
<Input
id="cnpj"
value={formData.cnpj || ''}
onChange={(e) => handleChange('cnpj', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="ie" className="text-sm font-medium text-muted-foreground">IE</Label>
<Input
id="ie"
value={formData.ie || ''}
onChange={(e) => handleChange('ie', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div className="md:col-span-2">
<Label htmlFor="endereco" className="text-sm font-medium text-muted-foreground">Endereço</Label>
<Input
id="endereco"
value={formData.endereco || ''}
onChange={(e) => handleChange('endereco', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="cidade" className="text-sm font-medium text-muted-foreground">Cidade</Label>
<Input
id="cidade"
value={formData.cidade || ''}
onChange={(e) => handleChange('cidade', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="estado" className="text-sm font-medium text-muted-foreground">Estado</Label>
<Input
id="estado"
value={formData.estado || ''}
onChange={(e) => handleChange('estado', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
</div>
</CardContent>
</Card>
{/* Informações da Obra */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<Building2 className="w-5 h-5" />
Informações da Obra
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3">
<div>
<Label htmlFor="eng_responsavel" className="text-sm font-medium text-muted-foreground">Eng. Responsável</Label>
<Input
id="eng_responsavel"
value={formData.eng_responsavel || ''}
onChange={(e) => handleChange('eng_responsavel', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="telefone_obra" className="text-sm font-medium text-muted-foreground">Telefone</Label>
<Input
id="telefone_obra"
value={formData.telefone_obra || ''}
onChange={(e) => handleChange('telefone_obra', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div className="md:col-span-2">
<Label htmlFor="email_obra_responsavel" className="text-sm font-medium text-muted-foreground">E-mail</Label>
<Input
id="email_obra_responsavel"
type="email"
value={formData.email_obra_responsavel || ''}
onChange={(e) => handleChange('email_obra_responsavel', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div className="md:col-span-2">
<Label htmlFor="endereco_obra" className="text-sm font-medium text-muted-foreground">Endereço da Obra</Label>
<Input
id="endereco_obra"
value={formData.endereco_obra || ''}
onChange={(e) => handleChange('endereco_obra', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="cep_obra" className="text-sm font-medium text-muted-foreground">CEP</Label>
<Input
id="cep_obra"
value={formData.cep_obra || ''}
onChange={(e) => handleChange('cep_obra', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="bairro_obra" className="text-sm font-medium text-muted-foreground">Bairro</Label>
<Input
id="bairro_obra"
value={formData.bairro_obra || ''}
onChange={(e) => handleChange('bairro_obra', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="cidade_obra" className="text-sm font-medium text-muted-foreground">Cidade</Label>
<Input
id="cidade_obra"
value={formData.cidade_obra || ''}
onChange={(e) => handleChange('cidade_obra', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div>
<Label htmlFor="estado_obra" className="text-sm font-medium text-muted-foreground">Estado (UF)</Label>
<Input
id="estado_obra"
value={formData.estado_obra || ''}
onChange={(e) => handleChange('estado_obra', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
/>
</div>
<div className="md:col-span-4">
<Label htmlFor="observacoes_obra" className="text-sm font-medium text-muted-foreground">Observações Gerais</Label>
<Textarea
id="observacoes_obra"
value={formData.observacoes_obra || ''}
onChange={(e) => handleChange('observacoes_obra', e.target.value)}
className="mt-1 bg-input border-border text-foreground"
rows={3}
/>
</div>
</div>
</CardContent>
</Card>
{/* Tipos de Projetos */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<Settings2 className="w-5 h-5" />
Tipos de Projetos
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ key: 'tipo_estrutural', label: 'Estrutural' },
{ key: 'tipo_residencial', label: 'Residencial' },
{ key: 'tipo_espacial', label: 'Espacial' },
{ key: 'tipo_comercial', label: 'Comercial' },
{ key: 'tipo_grades', label: 'Grades' },
{ key: 'tipo_industrial', label: 'Industrial' },
{ key: 'tipo_cobertura', label: 'Cobertura' },
{ key: 'tipo_com_montagem', label: 'Com Montagem' },
].map(({ key, label }) => (
<div key={key} className="flex items-center space-x-2">
<Checkbox
checked={formData[key as keyof FormData] as boolean || false}
onCheckedChange={(checked) => handleChange(key as keyof FormData, checked)}
/>
<Label className="text-sm">{label}</Label>
</div>
))}
</div>
</CardContent>
</Card>
{/* Documentos Fornecidos */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<FileText className="w-5 h-5" />
Documentos Fornecidos
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ key: 'doc_calculo', label: 'Cálculo' },
{ key: 'doc_projeto', label: 'Projeto' },
{ key: 'doc_detalhamento', label: 'Detalhamento' },
{ key: 'doc_cronograma', label: 'Cronograma' },
{ key: 'doc_normas', label: 'Normas' },
{ key: 'doc_especif_tecnicas', label: 'Especif. Técnicas' },
{ key: 'doc_catalogo', label: 'Catálogo' },
{ key: 'doc_fotos', label: 'Fotos' },
].map(({ key, label }) => (
<div key={key} className="flex items-center space-x-2">
<Checkbox
checked={formData[key as keyof FormData] as boolean || false}
onCheckedChange={(checked) => handleChange(key as keyof FormData, checked)}
/>
<Label className="text-sm">{label}</Label>
</div>
))}
</div>
</CardContent>
</Card>
{/* Alterações - Como botão popup */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<Settings2 className="w-5 h-5" />
Alterações da OF
</CardTitle>
</CardHeader>
<CardContent>
<AlteracoesPopup formData={formData} setFormData={setFormData} />
</CardContent>
</Card>
{/* Informações Técnicas Completas */}
<Card className="bg-card border-border">
<CardHeader className="pb-3">
<CardTitle className="text-lg flex items-center gap-2 text-card-foreground">
<Settings2 className="w-5 h-5" />
Informações Técnicas
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{renderTechnicalInfoField('Cálculo Estrutural', 'info_calculo_estrutural', formData.info_calculo_estrutural)}
{renderTechnicalInfoField('Projeto Básico', 'info_projeto_basico', formData.info_projeto_basico)}
{renderTechnicalInfoField('Detalhamento', 'info_detalhamento', formData.info_detalhamento)}
{renderTechnicalInfoField('Matéria Prima', 'info_materia_prima', formData.info_materia_prima)}
{renderTechnicalInfoField('Fabricação', 'info_fabricacao', formData.info_fabricacao)}
{renderTechnicalInfoField('Grades de Piso', 'info_grades_piso', formData.info_grades_piso)}
{renderTechnicalInfoField('Jateamento', 'info_jateamento', formData.info_jateamento)}
{renderTechnicalInfoField('Pintura Base', 'info_pintura_base', formData.info_pintura_base)}
{renderTechnicalInfoField('Pintura Intermediária', 'info_pintura_inter', formData.info_pintura_inter)}
{renderTechnicalInfoField('Pintura Acabamento', 'info_pintura_acabamento', formData.info_pintura_acabamento)}
{renderTechnicalInfoField('Galvanização', 'info_galvanizacao', formData.info_galvanizacao)}
{renderTechnicalInfoField('Embalagem', 'info_embalagem', formData.info_embalagem)}
{renderTechnicalInfoField('Transporte', 'info_transporte', formData.info_transporte)}
{renderTechnicalInfoField('Inspeção', 'info_inspecao', formData.info_inspecao)}
{renderTechnicalInfoField('Ensaios de Laboratório', 'info_ensaios_lab', formData.info_ensaios_lab)}
{renderTechnicalInfoField('Databook', 'info_databook', formData.info_databook)}
{renderTechnicalInfoField('Pré-montagem', 'info_pre_montagem', formData.info_pre_montagem)}
{renderTechnicalInfoField('Placa Engenetal', 'info_placa_engenetal', formData.info_placa_engenetal)}
{renderTechnicalInfoField('Parafusos', 'info_parafusos', formData.info_parafusos)}
{renderTechnicalInfoField('Chumbadores', 'info_chumbadores', formData.info_chumbadores)}
{renderTechnicalInfoField('Stud Bolt', 'info_stud_bolt', formData.info_stud_bolt)}
{renderTechnicalInfoField('Fornecimento Telhas', 'info_fornec_telhas', formData.info_fornec_telhas)}
{renderTechnicalInfoField('Montagem Telhas', 'info_montagem_telhas', formData.info_montagem_telhas)}
{renderTechnicalInfoField('Fornecimento Calhas', 'info_forn_calhas', formData.info_forn_calhas)}
{renderTechnicalInfoField('Montagem Calhas', 'info_mont_calhas', formData.info_mont_calhas)}
{renderTechnicalInfoField('Steel Deck', 'info_steel_deck', formData.info_steel_deck)}
{renderTechnicalInfoField('Fornecimento Wall', 'info_fornec_wall', formData.info_fornec_wall)}
{renderTechnicalInfoField('Montagem Wall', 'info_mont_wall', formData.info_mont_wall)}
{renderTechnicalInfoField('Outros Materiais', 'info_outros_materiais', formData.info_outros_materiais)}
</div>
</CardContent>
</Card>
</div>
{/* Preview Modal */}
<FichaTecnicaPreview
isOpen={showPreview}
onClose={() => setShowPreview(false)}
data={formData}
/>
</div>
);
};
export default CadastroOF;
+241
View File
@@ -0,0 +1,241 @@
import React, { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { toast } from 'sonner';
import { usePecas, Peca } from '@/hooks/usePecas';
import { usePecasP4Listener } from '@/hooks/usePecasP4Listener';
import { PecaForm } from '@/components/pecas/PecaForm';
import { PecasTable } from '@/components/pecas/PecasTable';
import { Package, Plus, RefreshCw } from 'lucide-react';
import { usePermissionControl } from '@/hooks/usePermissionControl';
export default function CadastroPecas() {
// Inicializar listener para peças P4
usePecasP4Listener();
const {
pecas,
loading,
ofNumbers,
savePeca,
updatePeca,
deletePeca,
importCSV,
importPecas,
undoLastImport,
deleteLastImport,
hasRecentImport,
loadPecas,
sincronizarPrioridades
} = usePecas();
const { canCreate, canEdit, canDelete } = usePermissionControl();
const [saving, setSaving] = useState(false);
const [syncing, setSyncing] = useState(false);
const [editingPeca, setEditingPeca] = useState<Peca | null>(null);
const [showForm, setShowForm] = useState(false);
const handleSave = async (formData: any) => {
if (!canCreate()) return false;
setSaving(true);
try {
const success = await savePeca(formData);
if (success) {
setShowForm(false);
}
return success;
} finally {
setSaving(false);
}
};
const handleUpdate = async (id: string, formData: any) => {
if (!canEdit()) return false;
setSaving(true);
try {
const success = await updatePeca(id, formData);
if (success) {
setEditingPeca(null);
setShowForm(false);
}
return success;
} finally {
setSaving(false);
}
};
const handleDelete = async (pecaId: string) => {
if (!canDelete()) return;
if (confirm('Tem certeza que deseja apagar esta peça?')) {
await deletePeca(pecaId);
}
};
const handleEdit = (peca: Peca) => {
if (!canEdit()) return;
setEditingPeca(peca);
setShowForm(true);
};
const handleCancelEdit = () => {
setEditingPeca(null);
setShowForm(false);
};
const handleDeleteLastImport = async () => {
if (!canDelete()) return;
if (confirm('Tem certeza que deseja apagar todas as peças da última importação?')) {
await deleteLastImport();
}
};
const handleOpenComponentPopup = (pecaId: string) => {
console.log('Abrir popup de componentes para peça:', pecaId);
toast.info('Funcionalidade de componentes será implementada em breve');
};
const handleImportPecas = async (pecasData: any[]) => {
if (!canCreate()) {
toast.error('Você não tem permissão para importar peças');
throw new Error('Sem permissão');
}
try {
await importPecas(pecasData);
} catch (error) {
throw error;
}
};
const handleImportCSV = async (file: File): Promise<boolean> => {
if (!canCreate()) {
toast.error('Você não tem permissão para importar peças');
return false;
}
try {
await importCSV(file);
return true;
} catch (error) {
console.error('Erro ao importar CSV:', error);
return false;
}
};
const handleSyncPriorities = async () => {
setSyncing(true);
try {
await sincronizarPrioridades();
toast.success('Prioridades sincronizadas com sucesso!');
} catch (error) {
console.error('Erro ao sincronizar prioridades:', error);
toast.error('Erro ao sincronizar prioridades');
} finally {
setSyncing(false);
}
};
if (loading) {
return (
<div className="min-h-screen bg-background p-6">
<div className="flex items-center justify-center h-64">
<div className="text-lg text-foreground">Carregando cadastro de peças...</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">Cadastro de Peças</h1>
<p className="text-muted-foreground">
Gerencie o cadastro de peças do sistema.
<span className="text-sm block mt-1">
As prioridades são controladas automaticamente pelo sistema de Prioridades de Fabricação e são sincronizadas em tempo real.
</span>
</p>
</div>
<div className="flex items-center gap-4">
<Badge variant="secondary" className="bg-secondary text-secondary-foreground border-border">
{pecas.length} {pecas.length === 1 ? 'peça' : 'peças'}
</Badge>
<Button
onClick={handleSyncPriorities}
disabled={syncing}
variant="outline"
className="flex items-center gap-2"
>
<RefreshCw className={`h-4 w-4 ${syncing ? 'animate-spin' : ''}`} />
{syncing ? 'Sincronizando...' : 'Sincronizar Prioridades'}
</Button>
{canCreate() && (
<Button
onClick={() => setShowForm(true)}
className="bg-primary hover:bg-primary/90 text-primary-foreground flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Nova Peça
</Button>
)}
</div>
</div>
{/* Form Card */}
{showForm && canCreate() && (
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-card-foreground flex items-center gap-2">
<Package className="h-5 w-5" />
{editingPeca ? 'Editar Peça' : 'Nova Peça'}
</CardTitle>
</CardHeader>
<CardContent>
<PecaForm
ofNumbers={ofNumbers}
onSave={handleSave}
onUpdate={handleUpdate}
onImportCSV={handleImportCSV}
saving={saving}
editingPeca={editingPeca}
onCancelEdit={handleCancelEdit}
hasRecentImport={hasRecentImport}
onDeleteLastImport={handleDeleteLastImport}
pecas={pecas}
onImportPecas={handleImportPecas}
onUndoLastImport={undoLastImport}
canImport={canCreate()}
canDelete={canDelete()}
/>
</CardContent>
</Card>
)}
{/* Table Card */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-card-foreground flex items-center gap-2">
<Package className="h-5 w-5" />
Peças Cadastradas
</CardTitle>
</CardHeader>
<CardContent>
<PecasTable
pecas={pecas}
onOpenComponentPopup={handleOpenComponentPopup}
onDeletePeca={canDelete() ? handleDelete : undefined}
onEditPeca={canEdit() ? handleEdit : undefined}
onDeleteLastImport={canDelete() ? handleDeleteLastImport : undefined}
hasRecentImport={hasRecentImport}
canEdit={canEdit()}
canDelete={canDelete()}
/>
</CardContent>
</Card>
</div>
</div>
);
}
+287
View File
@@ -0,0 +1,287 @@
import React, { useState, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowLeft, Plus, Package } from 'lucide-react';
import { PecaForm } from '@/components/pecas/PecaForm';
import { PecasTable } from '@/components/pecas/PecasTable';
import { usePecas, Peca } from '@/hooks/usePecas';
import { toast } from 'sonner';
import { usePermissionControl } from '@/hooks/usePermissionControl';
export default function CadastroPecasFiltrado() {
const { ofNumber } = useParams<{ ofNumber: string }>();
const navigate = useNavigate();
const ofSelecionada = ofNumber || '';
const { canCreate, canEdit, canDelete } = usePermissionControl();
const {
pecas,
loading,
ofNumbers,
savePeca,
updatePeca,
deletePeca,
importCSV,
importPecas,
undoLastImport,
deleteLastImport,
hasRecentImport,
batchUpdatePecas,
loadPecas
} = usePecas();
const [saving, setSaving] = useState(false);
const [editingPeca, setEditingPeca] = useState<Peca | null>(null);
const [showForm, setShowForm] = useState(false);
// Filtrar peças pela OF selecionada
const pecasFiltradas = pecas.filter(peca => peca.of_number === ofSelecionada);
const handleSave = async (formData: any) => {
if (!canCreate()) return false;
setSaving(true);
try {
// Garantir que a peça seja salva com a OF selecionada
const formDataComOF = { ...formData, of_number: ofSelecionada };
const success = await savePeca(formDataComOF);
if (success) {
setShowForm(false);
// Reload data after successful save
loadPecas();
}
return success;
} finally {
setSaving(false);
}
};
const handleUpdate = async (id: string, formData: any) => {
if (!canEdit()) return false;
setSaving(true);
try {
const success = await updatePeca(id, formData);
if (success) {
setEditingPeca(null);
setShowForm(false);
// Reload data after successful update
loadPecas();
}
return success;
} finally {
setSaving(false);
}
};
const handleDelete = async (pecaId: string) => {
if (!canDelete()) return;
if (confirm('Tem certeza que deseja apagar esta peça?')) {
const success = await deletePeca(pecaId);
if (success) {
// Reload data after successful delete
loadPecas();
}
}
};
const handleEdit = (peca: Peca) => {
if (!canEdit()) return;
setEditingPeca(peca);
setShowForm(true);
};
const handleCancelEdit = () => {
setEditingPeca(null);
setShowForm(false);
};
const handleDeleteLastImport = async () => {
if (!canDelete()) return;
if (confirm('Tem certeza que deseja apagar todas as peças da última importação?')) {
await deleteLastImport();
// Reload data after successful delete
loadPecas();
}
};
const handleOpenComponentPopup = (pecaId: string) => {
console.log('Abrir popup de componentes para peça:', pecaId);
toast.info('Funcionalidade de componentes será implementada em breve');
};
const handleImportPecas = async (pecasData: any[]) => {
if (!canCreate()) {
toast.error('Você não tem permissão para importar peças');
throw new Error('Sem permissão');
}
try {
// Adicionar a OF selecionada a todas as peças importadas
const pecasComOF = pecasData.map(peca => ({
...peca,
of_number: ofSelecionada
}));
await importPecas(pecasComOF);
// Reload data after successful import
loadPecas();
} catch (error) {
throw error;
}
};
const handleImportCSV = async (file: File): Promise<boolean> => {
if (!canCreate()) {
toast.error('Você não tem permissão para importar peças');
return false;
}
try {
await importCSV(file);
// Reload data after successful import
loadPecas();
return true;
} catch (error) {
console.error('Erro ao importar CSV:', error);
return false;
}
};
const handleBatchUpdatePecas = async (pecaIds: string[], updates: any) => {
if (!canEdit()) {
toast.error('Você não tem permissão para editar peças');
throw new Error('Sem permissão');
}
try {
await batchUpdatePecas(pecaIds, updates);
// Reload data after successful batch update
loadPecas();
} catch (error) {
throw error;
}
};
const handleUndoLastImport = async () => {
await undoLastImport();
// Reload data after successful undo
loadPecas();
};
const handleVoltar = () => {
navigate('/seletor-of');
};
useEffect(() => {
if (!ofSelecionada) {
navigate('/seletor-of');
}
}, [ofSelecionada, navigate]);
if (loading) {
return (
<div className="min-h-screen bg-background p-6">
<div className="flex items-center justify-center h-64">
<div className="text-lg text-foreground">Carregando cadastro de peças...</div>
</div>
</div>
);
}
if (!ofSelecionada) {
return null;
}
return (
<div className="min-h-screen bg-background p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-4 mb-2">
<h1 className="text-3xl font-bold text-foreground">Cadastro de Peças</h1>
<Badge variant="secondary" className="text-lg px-3 py-1">
OF: {ofSelecionada}
</Badge>
</div>
<p className="text-muted-foreground">Gerencie as peças da OF selecionada</p>
</div>
<div className="flex items-center gap-4">
<Button
onClick={handleVoltar}
variant="outline"
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Voltar
</Button>
<Badge variant="secondary" className="bg-secondary text-secondary-foreground border-border">
{pecasFiltradas.length} {pecasFiltradas.length === 1 ? 'peça' : 'peças'}
</Badge>
{canCreate() && (
<Button
onClick={() => setShowForm(true)}
className="bg-primary hover:bg-primary/90 text-primary-foreground flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Nova Peça
</Button>
)}
</div>
</div>
{/* Form Card */}
{showForm && canCreate() && (
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-card-foreground flex items-center gap-2">
<Package className="h-5 w-5" />
{editingPeca ? 'Editar Peça' : 'Nova Peça'} - OF: {ofSelecionada}
</CardTitle>
</CardHeader>
<CardContent>
<PecaForm
ofNumbers={[ofSelecionada]} // Apenas a OF selecionada
onSave={handleSave}
onUpdate={handleUpdate}
onImportCSV={handleImportCSV}
saving={saving}
editingPeca={editingPeca}
onCancelEdit={handleCancelEdit}
hasRecentImport={hasRecentImport}
onDeleteLastImport={handleDeleteLastImport}
pecas={pecasFiltradas}
onImportPecas={handleImportPecas}
onUndoLastImport={handleUndoLastImport}
canImport={canCreate()}
canDelete={canDelete()}
/>
</CardContent>
</Card>
)}
{/* Table Card */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-card-foreground flex items-center gap-2">
<Package className="h-5 w-5" />
Peças da OF {ofSelecionada}
</CardTitle>
</CardHeader>
<CardContent>
<PecasTable
pecas={pecasFiltradas}
onOpenComponentPopup={handleOpenComponentPopup}
onDeletePeca={canDelete() ? handleDelete : undefined}
onEditPeca={canEdit() ? handleEdit : undefined}
onDeleteLastImport={canDelete() ? handleDeleteLastImport : undefined}
onBatchUpdatePecas={canEdit() ? handleBatchUpdatePecas : undefined}
hasRecentImport={hasRecentImport}
canEdit={canEdit()}
canDelete={canDelete()}
ofNumber={ofSelecionada}
/>
</CardContent>
</Card>
</div>
</div>
);
}
+157
View File
@@ -0,0 +1,157 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { BookOpen, Plus } from 'lucide-react';
import { useCatalogos } from '@/hooks/useCatalogos';
import { useUserRole } from '@/hooks/useUserRole';
import { CatalogosTable } from '@/components/catalogos/CatalogosTable';
import { CatalogoModal } from '@/components/catalogos/CatalogoModal';
import { CatalogoPreviews } from '@/components/catalogos/CatalogoPreviews';
import { CatalogosFilters } from '@/components/catalogos/CatalogosFilters';
import { DocumentViewer } from '@/components/catalogos/DocumentViewer';
import { Catalogo } from '@/hooks/useCatalogos';
const Catalogos = () => {
const { isAdmin } = useUserRole();
const {
catalogos,
loading,
searchTerm,
setSearchTerm,
categoriaFilter,
setCategoriaFilter,
disciplinaFilter,
setDisciplinaFilter,
createCatalogo,
updateCatalogo,
deleteCatalogo,
clearFilters
} = useCatalogos();
const [showModal, setShowModal] = useState(false);
const [editingCatalogo, setEditingCatalogo] = useState(null);
const [viewMode, setViewMode] = useState<'table' | 'preview'>('table');
const [viewingCatalogo, setViewingCatalogo] = useState<Catalogo | null>(null);
const [showDocumentViewer, setShowDocumentViewer] = useState(false);
const handleEdit = (catalogo: any) => {
setEditingCatalogo(catalogo);
setShowModal(true);
};
const handleView = (catalogo: Catalogo) => {
if (catalogo.arquivo_urls && catalogo.arquivo_urls.length > 0) {
setViewingCatalogo(catalogo);
setShowDocumentViewer(true);
}
};
const handleCloseModal = () => {
setShowModal(false);
setEditingCatalogo(null);
};
const handleCloseDocumentViewer = () => {
setShowDocumentViewer(false);
setViewingCatalogo(null);
};
const handleSave = async (data: any) => {
try {
if (editingCatalogo) {
await updateCatalogo(editingCatalogo.id, data);
} else {
await createCatalogo(data);
}
handleCloseModal();
} catch (error) {
console.error('Erro ao salvar catálogo:', error);
}
};
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-slate-900 dark:text-white mb-2">Catálogos</h1>
<p className="text-slate-600 dark:text-slate-400">Gerencie documentos técnicos e catálogos</p>
</div>
{isAdmin && (
<Button
onClick={() => setShowModal(true)}
className="bg-green-600 hover:bg-green-700"
>
<Plus className="w-4 h-4 mr-2" />
Novo Documento
</Button>
)}
</div>
<CatalogosFilters
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
categoriaFilter={categoriaFilter}
setCategoriaFilter={setCategoriaFilter}
disciplinaFilter={disciplinaFilter}
setDisciplinaFilter={setDisciplinaFilter}
onClearFilters={clearFilters}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
<CardHeader>
<CardTitle className="text-slate-900 dark:text-white flex items-center gap-2">
<BookOpen className="w-5 h-5" />
Documentos Disponíveis
</CardTitle>
</CardHeader>
<CardContent>
{viewMode === 'table' ? (
<CatalogosTable
catalogos={catalogos}
onEdit={handleEdit}
onDelete={deleteCatalogo}
onView={handleView}
canModify={isAdmin}
/>
) : (
<CatalogoPreviews
catalogos={catalogos}
onEdit={handleEdit}
onDelete={deleteCatalogo}
onView={handleView}
canModify={isAdmin}
/>
)}
</CardContent>
</Card>
{showModal && (
<CatalogoModal
catalogo={editingCatalogo}
onSave={handleSave}
onClose={handleCloseModal}
/>
)}
<DocumentViewer
catalogo={viewingCatalogo}
isOpen={showDocumentViewer}
onClose={handleCloseDocumentViewer}
/>
</div>
);
};
export default Catalogos;
+302
View File
@@ -0,0 +1,302 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Upload, Eye } from 'lucide-react';
import { toast } from 'sonner';
import { useAuth } from '@/hooks/useAuth';
import { useUserRole } from '@/hooks/useUserRole';
import { useBrandSettings } from '@/hooks/useBrandSettings';
import { useIconStyle } from '@/hooks/useIconStyle';
import { MenuBuilder } from '@/components/menu-builder/MenuBuilder';
import { PrioridadesConfig } from '@/components/configuracoes/PrioridadesConfig';
const FONT_OPTIONS = [{
value: 'Arial',
label: 'Arial - Padrão clássico'
}, {
value: 'Helvetica',
label: 'Helvetica - Moderna e limpa'
}, {
value: 'SF Pro Display',
label: 'SF Pro - Fonte do sistema Apple'
}, {
value: 'system-ui',
label: 'Fonte do Sistema Operacional'
}];
const Configuracoes = () => {
const {
user
} = useAuth();
const {
isAdmin
} = useUserRole();
const {
brandSettings,
setBrandSettings,
saveBrandSettings,
uploadLogo,
isLoading: brandLoading
} = useBrandSettings();
const { iconStyle, setIconStyle } = useIconStyle();
const [isLoading, setIsLoading] = useState(false);
const [logoFile, setLogoFile] = useState<File | null>(null);
const [logoPreview, setLogoPreview] = useState<string | null>(null);
useEffect(() => {
if (brandSettings.logo_url) {
setLogoPreview(brandSettings.logo_url);
}
}, [brandSettings.logo_url]);
const handleLogoUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
// 5MB limit
toast.error('Arquivo muito grande. Máximo 5MB.');
return;
}
if (!file.type.startsWith('image/')) {
toast.error('Por favor, selecione um arquivo de imagem.');
return;
}
setLogoFile(file);
const reader = new FileReader();
reader.onload = e => {
setLogoPreview(e.target?.result as string);
};
reader.readAsDataURL(file);
}
};
const handleSaveSettings = async () => {
if (!isAdmin) {
toast.error('Acesso negado. Apenas administradores podem alterar essas configurações.');
return;
}
setIsLoading(true);
try {
let logoUrl = brandSettings.logo_url;
// Upload logo if a new file was selected
if (logoFile) {
const uploadResult = await uploadLogo(logoFile);
if (uploadResult.error) {
setIsLoading(false);
return;
}
logoUrl = uploadResult.data?.publicUrl || null;
}
// Save brand settings
const settingsToSave = {
...brandSettings,
logo_url: logoUrl
};
const result = await saveBrandSettings(settingsToSave);
if (result.error) {
setIsLoading(false);
return;
}
// Apply font globally
const fontFamily = brandSettings.font_family === 'system-ui' ? 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif' : brandSettings.font_family;
document.documentElement.style.setProperty('--font-family', fontFamily);
document.body.style.fontFamily = fontFamily;
// Clear file selection
setLogoFile(null);
} catch (error) {
console.error('Error saving brand settings:', error);
toast.error('Erro ao salvar configurações');
} finally {
setIsLoading(false);
}
};
const handleFontChange = (value: string) => {
setBrandSettings(prev => ({
...prev,
font_family: value
}));
// Preview da fonte
const fontFamily = value === 'system-ui' ? 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif' : value;
document.documentElement.style.setProperty('--font-family', fontFamily);
document.body.style.fontFamily = fontFamily;
};
if (!isAdmin) {
return <div className="space-y-6 p-4 sm:p-6 bg-background min-h-screen">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">Configurações</h1>
<p className="text-muted-foreground">Configurações gerais do sistema</p>
</div>
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
<CardHeader>
<CardTitle className="text-foreground">Acesso Restrito</CardTitle>
</CardHeader>
<CardContent>
<p className="text-muted-foreground">
Apenas administradores podem acessar as configurações avançadas do sistema.
</p>
</CardContent>
</Card>
</div>;
}
return <div className="space-y-6 p-4 sm:p-6 bg-background min-h-screen">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">Configurações Gerais</h1>
<p className="text-muted-foreground">Configurações gerais do sistema</p>
</div>
{/* Personalização de Marca */}
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
<CardHeader>
<CardTitle className="text-foreground">Personalização de Marca</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Nome da Empresa */}
<div className="space-y-2">
<Label htmlFor="company-name" className="text-foreground">
Nome da Empresa
</Label>
<Input
id="company-name"
value={brandSettings.company_name}
onChange={e => setBrandSettings(prev => ({
...prev,
company_name: e.target.value
}))}
placeholder="Digite o nome da empresa"
className="bg-white border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-700 dark:text-white"
/>
</div>
<div className="space-y-2">
<Label className="text-foreground">Logotipo da Empresa</Label>
<div className="space-y-4">
<div className="flex items-center gap-3">
<Button
type="button"
variant="outline"
className="bg-background border-border text-foreground hover:bg-accent dark:bg-slate-800 dark:border-slate-700 dark:text-white"
onClick={() => document.getElementById('logo-upload')?.click()}
>
<Upload className="w-4 h-4 mr-2" />
Selecionar Logo
</Button>
<span className="text-muted-foreground text-sm">
JPG, PNG ou SVG. Máximo 5MB.
</span>
</div>
<input id="logo-upload" type="file" accept="image/*" onChange={handleLogoUpload} className="hidden" />
{logoPreview && <div className="border border-slate-200 rounded-lg p-4 bg-slate-50 dark:bg-slate-800 dark:border-slate-700">
<p className="text-muted-foreground text-sm mb-2 flex items-center gap-2">
<Eye className="w-4 h-4" />
Pré-visualização
</p>
<img src={logoPreview} alt="Logo preview" className="max-w-32 max-h-16 object-contain" />
</div>}
</div>
</div>
<div className="space-y-2">
<Label className="text-foreground">Estilo de Fonte</Label>
<Select value={brandSettings.font_family} onValueChange={handleFontChange}>
<SelectTrigger className="bg-white border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-700 dark:text-white">
<SelectValue placeholder="Selecione uma fonte" />
</SelectTrigger>
<SelectContent className="bg-white border-slate-300 dark:bg-slate-800 dark:border-slate-700">
{FONT_OPTIONS.map(font => <SelectItem key={font.value} value={font.value} className="text-foreground dark:text-white">
<span style={{
fontFamily: font.value === 'system-ui' ? 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' : font.value
}}>
{font.label}
</span>
</SelectItem>)}
</SelectContent>
</Select>
<div className="mt-3 p-3 rounded border border-slate-200 bg-slate-50 dark:bg-slate-800 dark:border-slate-700">
<p className="text-muted-foreground text-sm mb-2">Pré-visualização da fonte:</p>
<p
style={{
fontFamily: brandSettings.font_family === 'system-ui'
? 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif'
: brandSettings.font_family
}}
className="text-foreground"
>
Este é um exemplo de como o texto aparecerá com a fonte selecionada.
</p>
</div>
</div>
<div className="pt-4">
<Button
onClick={handleSaveSettings}
disabled={isLoading || brandLoading}
className="bg-primary hover:bg-primary/90 text-primary-foreground"
>
{isLoading ? 'Salvando...' : 'Salvar Configurações'}
</Button>
</div>
</CardContent>
</Card>
{/* Personalização de Interface */}
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
<CardHeader>
<CardTitle className="text-foreground">Personalização de Interface</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Estilo dos Ícones */}
<div className="space-y-2">
<Label className="text-foreground">Estilo dos Ícones do Menu</Label>
<Select value={iconStyle} onValueChange={setIconStyle}>
<SelectTrigger className="bg-white border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-700 dark:text-white">
<SelectValue placeholder="Selecione o estilo dos ícones" />
</SelectTrigger>
<SelectContent className="bg-white border-slate-300 dark:bg-slate-800 dark:border-slate-700">
<SelectItem value="colorful" className="text-foreground dark:text-white">
Coloridos (Cores Originais)
</SelectItem>
<SelectItem value="themed" className="text-foreground dark:text-white">
Temáticos (Cor Principal do Tema)
</SelectItem>
<SelectItem value="white" className="text-foreground dark:text-white">
Brancos (Monocromático)
</SelectItem>
</SelectContent>
</Select>
<div className="mt-3 p-3 rounded border border-slate-200 bg-slate-50 dark:bg-slate-800 dark:border-slate-700">
<p className="text-muted-foreground text-sm mb-2">Configuração atual:</p>
<p className="text-foreground">
{iconStyle === 'colorful' && 'Ícones coloridos (cores originais dos ícones)'}
{iconStyle === 'themed' && 'Ícones temáticos (seguem a cor principal do tema)'}
{iconStyle === 'white' && 'Ícones brancos (estilo monocromático)'}
</p>
</div>
</div>
</CardContent>
</Card>
{/* Configuração de Prioridades */}
<PrioridadesConfig />
{/* Menu Builder */}
<MenuBuilder />
</div>;
};
export default Configuracoes;
+380
View File
@@ -0,0 +1,380 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { RefreshCw, Upload, FileText, Download, Settings, FileUp } from 'lucide-react';
import { toast } from 'sonner';
import TecnometalConverter from '@/components/conversores/TecnometalConverter';
import BocadConverter from '@/components/conversores/BocadConverter';
import AdvanceSteelConverter from '@/components/conversores/AdvanceSteelConverter';
import PromptsManager from '@/components/conversores/PromptsManager';
import FileImporter from '@/components/conversores/FileImporter';
import ConversaoGenericaModal from '@/components/conversores/ConversaoGenericaModal';
const ConversoresDados = () => {
const [isConverting, setIsConverting] = useState(false);
const [selectedConverter, setSelectedConverter] = useState<string | null>(null);
const [showTecnometalConverter, setShowTecnometalConverter] = useState(false);
const [showBocadConverter, setShowBocadConverter] = useState(false);
const [showAdvanceSteelConverter, setShowAdvanceSteelConverter] = useState(false);
const [showPromptsManager, setShowPromptsManager] = useState(false);
const [showFileImporter, setShowFileImporter] = useState(false);
const [showGenericConverter, setShowGenericConverter] = useState(false);
const converters = [
{ id: 'tecnometal', name: 'Conversão Tecnometal', description: 'Converte arquivos no formato Tecnometal' },
{ id: 'bocad', name: 'Conversão Bocad', description: 'Converte arquivos no formato Bocad' },
{ id: 'adv_steel', name: 'Conversão Adv_Steel', description: 'Converte arquivos no formato Advance Steel' },
{ id: 'tekla', name: 'Conversão Tekla', description: 'Converte arquivos no formato Tekla' },
{ id: 'generica', name: 'Conversão Genérica', description: 'Conversão para formatos padronizados' }
];
const handleFileUpload = async (converterId: string, event: React.ChangeEvent<HTMLInputElement>) => {
if (converterId === 'tecnometal') {
setShowTecnometalConverter(true);
return;
}
if (converterId === 'bocad') {
setShowBocadConverter(true);
return;
}
if (converterId === 'adv_steel') {
setShowAdvanceSteelConverter(true);
return;
}
if (converterId === 'generica') {
setShowGenericConverter(true);
return;
}
const file = event.target.files?.[0];
if (!file) return;
setIsConverting(true);
setSelectedConverter(converterId);
try {
// Simular processamento por enquanto
await new Promise(resolve => setTimeout(resolve, 2000));
toast.success(`Arquivo convertido com sucesso usando ${converters.find(c => c.id === converterId)?.name}!`);
// Reset file input
event.target.value = '';
} catch (error) {
toast.error('Erro ao converter arquivo');
console.error('Conversion error:', error);
} finally {
setIsConverting(false);
setSelectedConverter(null);
}
};
const handleTecnometalClick = () => {
setShowTecnometalConverter(true);
};
const handleBocadClick = () => {
setShowBocadConverter(true);
};
const handleAdvanceSteelClick = () => {
setShowAdvanceSteelConverter(true);
};
const handleGoBack = () => {
setShowTecnometalConverter(false);
setShowBocadConverter(false);
setShowAdvanceSteelConverter(false);
setShowPromptsManager(false);
setShowFileImporter(false);
};
// Tela do Gerenciador de Prompts
if (showPromptsManager) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={handleGoBack}
className="bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
Voltar
</Button>
<div>
<h1 className="text-3xl font-bold text-white mb-2">Gerenciar Prompts</h1>
<p className="text-slate-400">Configure instruções para conversão de arquivos</p>
</div>
</div>
<PromptsManager />
</div>
);
}
// Tela do Importador de Arquivos
if (showFileImporter) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={handleGoBack}
className="bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
Voltar
</Button>
<div>
<h1 className="text-3xl font-bold text-white mb-2">Importar Arquivo</h1>
<p className="text-slate-400">Converta arquivos externos para CSV com instruções personalizadas</p>
</div>
</div>
<FileImporter />
</div>
);
}
if (showTecnometalConverter) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={handleGoBack}
className="bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
Voltar
</Button>
<div>
<h1 className="text-3xl font-bold text-white mb-2">Conversão Tecnometal</h1>
<p className="text-slate-400">Processador e Editor de Lista de Peças para CSV</p>
</div>
</div>
<TecnometalConverter />
</div>
);
}
if (showBocadConverter) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={handleGoBack}
className="bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
Voltar
</Button>
</div>
<BocadConverter />
</div>
);
}
if (showAdvanceSteelConverter) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button
variant="outline"
onClick={handleGoBack}
className="bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
Voltar
</Button>
</div>
<AdvanceSteelConverter />
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Conversores de Dados</h1>
<p className="text-slate-400">Ferramentas para conversão de diferentes formatos de arquivos</p>
</div>
{/* Novos botões principais */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<FileUp className="w-5 h-5" />
Ferramentas de Conversão Avançada
</CardTitle>
<CardDescription className="text-slate-300">
Importe arquivos externos e gerencie prompts de conversão personalizados
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Button
onClick={() => setShowFileImporter(true)}
variant="outline"
className="h-auto p-4 bg-slate-700/50 border-slate-600 text-white hover:bg-slate-700 flex flex-col items-center gap-2"
>
<Upload className="w-6 h-6 text-blue-400" />
<div className="text-center">
<div className="font-medium">Importar Arquivo</div>
<div className="text-xs text-slate-400 mt-1">
Converta planilhas e PDFs para CSV
</div>
</div>
</Button>
<Button
onClick={() => setShowPromptsManager(true)}
variant="outline"
className="h-auto p-4 bg-slate-700/50 border-slate-600 text-white hover:bg-slate-700 flex flex-col items-center gap-2"
>
<Settings className="w-6 h-6 text-green-400" />
<div className="text-center">
<div className="font-medium">Gerenciar Prompts</div>
<div className="text-xs text-slate-400 mt-1">
Configure instruções de conversão
</div>
</div>
</Button>
</div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<RefreshCw className="w-5 h-5" />
Conversores de Listas de Peças
</CardTitle>
<CardDescription className="text-slate-300">
Importe uma lista de peças para convertê-la em diferentes formatos, como PDF, CSV ou Imagem.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{converters.map((converter) => (
<Card key={converter.id} className="bg-slate-700/50 border-slate-600 hover:bg-slate-700/70 transition-colors">
<CardHeader className="pb-3">
<CardTitle className="text-sm text-white">{converter.name}</CardTitle>
<CardDescription className="text-xs text-slate-400">
{converter.description}
</CardDescription>
</CardHeader>
<CardContent className="pt-0">
{converter.id === 'tecnometal' ? (
<Button
onClick={handleTecnometalClick}
variant="outline"
className="w-full bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
<FileText className="w-4 h-4 mr-2" />
Abrir Conversor
</Button>
) : converter.id === 'bocad' ? (
<Button
onClick={handleBocadClick}
variant="outline"
className="w-full bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
<FileText className="w-4 h-4 mr-2" />
Abrir Conversor
</Button>
) : converter.id === 'adv_steel' ? (
<Button
onClick={handleAdvanceSteelClick}
variant="outline"
className="w-full bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
<FileText className="w-4 h-4 mr-2" />
Abrir Conversor
</Button>
) : converter.id === 'generica' ? (
<Button
onClick={() => setShowGenericConverter(true)}
variant="outline"
className="w-full bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
>
<Upload className="w-4 h-4 mr-2" />
Selecionar Arquivo
</Button>
) : (
<div className="relative">
<input
type="file"
id={`file-${converter.id}`}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
accept=".csv,.txt,.xls,.xlsx"
onChange={(e) => handleFileUpload(converter.id, e)}
disabled={isConverting}
/>
<Button
variant="outline"
className="w-full bg-slate-600 border-slate-500 text-white hover:bg-slate-500"
disabled={isConverting && selectedConverter === converter.id}
>
{isConverting && selectedConverter === converter.id ? (
<>
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
Convertendo...
</>
) : (
<>
<Upload className="w-4 h-4 mr-2" />
Selecionar Arquivo
</>
)}
</Button>
</div>
)}
</CardContent>
</Card>
))}
</div>
</CardContent>
</Card>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<FileText className="w-5 h-5" />
Formatos Suportados
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<h4 className="text-white font-medium mb-2">Entrada</h4>
<ul className="text-slate-300 text-sm space-y-1">
<li> Arquivos CSV (.csv)</li>
<li> Arquivos de texto (.txt)</li>
<li> Planilhas Excel (.xls, .xlsx)</li>
<li> Documentos PDF (.pdf)</li>
<li> Formatos proprietários</li>
</ul>
</div>
<div>
<h4 className="text-white font-medium mb-2">Saída</h4>
<ul className="text-slate-300 text-sm space-y-1">
<li> PDF para impressão</li>
<li> CSV padronizado</li>
<li> Imagens (PNG, JPG)</li>
<li> Planilhas formatadas</li>
<li> JSON estruturado</li>
</ul>
</div>
</div>
</CardContent>
</Card>
{/* Modal de Conversão Genérica */}
<ConversaoGenericaModal
open={showGenericConverter}
onOpenChange={setShowGenericConverter}
/>
</div>
);
};
export default ConversoresDados;
+308
View File
@@ -0,0 +1,308 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Calendar, Clock, User, FileText, Filter, Search } from 'lucide-react';
import { useCronogramas } from '@/hooks/useCronogramas';
import { CronogramaTable } from '@/components/cronograma/CronogramaTable';
import { CronogramaForm } from '@/components/cronograma/CronogramaForm';
import { CronogramaGantt } from '@/components/cronograma/CronogramaGantt';
import { CronogramaPDF } from '@/components/cronograma/CronogramaPDF';
import { useMobileResponsive } from '@/hooks/useMobileResponsive';
import { usePermissionControl } from '@/hooks/usePermissionControl';
import { CronogramaOf } from '@/types/cronograma';
const CronogramaOF = () => {
const { cronogramas, loading, loadCronogramas, deleteCronograma } = useCronogramas();
const { isMobile } = useMobileResponsive();
const { canCreate, canEdit, canDelete } = usePermissionControl();
const [activeTab, setActiveTab] = useState('cronogramas');
const [searchTerm, setSearchTerm] = useState('');
const [selectedOF, setSelectedOF] = useState<string>('');
const [showCronogramaForm, setShowCronogramaForm] = useState(false);
const [selectedCronograma, setSelectedCronograma] = useState<any>(null);
const [showGanttChart, setShowGanttChart] = useState(false);
const [showPDFGenerator, setShowPDFGenerator] = useState(false);
const [cronogramaForGantt, setCronogramaForGantt] = useState<CronogramaOf | null>(null);
const [cronogramaForPDF, setCronogramaForPDF] = useState<CronogramaOf | null>(null);
// Filter and sort cronogramas based on search term and ordered by OF
const filteredAndSortedCronogramas = cronogramas
?.filter(cronograma =>
cronograma.ordem_fabricacao?.num_of?.toLowerCase().includes(searchTerm.toLowerCase()) ||
cronograma.ordem_fabricacao?.descritivo?.toLowerCase().includes(searchTerm.toLowerCase())
)
.sort((a, b) => {
const ofA = a.ordem_fabricacao?.num_of || '';
const ofB = b.ordem_fabricacao?.num_of || '';
return ofA.localeCompare(ofB);
});
const handleEdit = (cronograma: any) => {
if (!canEdit()) return;
setSelectedCronograma(cronograma);
setShowCronogramaForm(true);
};
const handleDelete = (cronogramaId: string) => {
if (!canDelete()) return;
deleteCronograma(cronogramaId);
};
const handleViewChart = (cronograma: CronogramaOf) => {
setCronogramaForGantt(cronograma);
setShowGanttChart(true);
};
const handleViewPDF = (cronograma: CronogramaOf) => {
setCronogramaForPDF(cronograma);
setShowPDFGenerator(true);
};
const handleCloseCronogramaForm = () => {
setShowCronogramaForm(false);
setSelectedCronograma(null);
};
const handleCloseGanttChart = () => {
setShowGanttChart(false);
setCronogramaForGantt(null);
};
const handleClosePDFGenerator = () => {
setShowPDFGenerator(false);
setCronogramaForPDF(null);
};
const MobileCronogramaCard = ({ cronograma }: { cronograma: any }) => (
<Card className="w-full mb-4 bg-card border-border">
<CardHeader className="pb-3">
<div className="flex justify-between items-start">
<CardTitle className="text-lg text-card-foreground">
OF: {cronograma.ordem_fabricacao?.num_of || 'N/A'}
</CardTitle>
<Badge variant="outline" className="text-xs">
Rev. {cronograma.revisao || 1}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-2 text-sm">
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">Descrição:</span>
<span className="text-card-foreground">{cronograma.ordem_fabricacao?.descritivo || 'N/A'}</span>
</div>
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">Gestor:</span>
<span className="text-card-foreground">{cronograma.gestor_profile?.full_name || 'N/A'}</span>
</div>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">Processos:</span>
<span className="text-card-foreground">{cronograma.processos?.length || 0}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-muted-foreground">Peso Total:</span>
<span className="text-card-foreground">{cronograma.peso_total ? `${Number(cronograma.peso_total).toLocaleString('pt-BR')} kg` : 'N/A'}</span>
</div>
<div className="mt-3">
<span className="text-muted-foreground text-xs">Cronograma:</span>
<div className="mt-1 text-card-foreground">
{Array.isArray(cronograma.processos) && cronograma.processos.length > 0 ? (
<div className="text-xs">
{cronograma.processos.map((processo: any, index: number) => (
<div key={index} className="py-1 border-b border-border last:border-0">
{processo.nome_processo || `Processo ${index + 1}`}
</div>
))}
</div>
) : (
'Nenhum processo definido'
)}
</div>
</div>
</div>
<div className="flex gap-2 pt-2 border-t border-border">
<Button
variant="outline"
size="sm"
onClick={() => handleViewChart(cronograma)}
className="flex-1 text-xs border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-700"
>
Gráfico
</Button>
<Button
variant="outline"
size="sm"
onClick={() => handleViewPDF(cronograma)}
className="flex-1 text-xs border-green-200 bg-green-50 hover:bg-green-100 text-green-700"
>
PDF
</Button>
{canEdit() && (
<Button
variant="outline"
size="sm"
onClick={() => handleEdit(cronograma)}
className="flex-1 text-xs"
>
Editar
</Button>
)}
</div>
</CardContent>
</Card>
);
if (loading) {
return (
<div className="p-3 sm:p-6">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-muted rounded w-1/4"></div>
<div className="h-32 bg-muted rounded"></div>
</div>
</div>
);
}
return (
<div className="p-3 sm:p-6 space-y-4 sm:space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-foreground">
Cronograma das OFs
</h1>
{/* Mobile Search */}
{isMobile && (
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="Buscar por OF ou descrição..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 text-sm"
/>
</div>
)}
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-4">
<TabsTrigger value="cronogramas" className="text-xs sm:text-sm">
Lista
</TabsTrigger>
<TabsTrigger value="novo" className="text-xs sm:text-sm">
Novo
</TabsTrigger>
</TabsList>
<TabsContent value="cronogramas" className="space-y-4">
{/* Desktop Search */}
{!isMobile && (
<div className="flex gap-4 items-center">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="Buscar por OF ou descrição..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
)}
{isMobile ? (
<div className="space-y-4">
{filteredAndSortedCronogramas && filteredAndSortedCronogramas.length > 0 ? (
filteredAndSortedCronogramas.map((cronograma) => (
<MobileCronogramaCard
key={cronograma.id}
cronograma={cronograma}
/>
))
) : (
<Card className="p-8 text-center">
<p className="text-muted-foreground">Nenhum cronograma encontrado.</p>
</Card>
)}
</div>
) : (
<div className="overflow-x-auto">
<CronogramaTable
cronogramas={filteredAndSortedCronogramas || []}
onEdit={canEdit() ? handleEdit : () => {}}
onDelete={canDelete() ? handleDelete : () => {}}
onViewChart={handleViewChart}
onViewPDF={handleViewPDF}
canEdit={canEdit()}
canDelete={canDelete()}
/>
</div>
)}
</TabsContent>
<TabsContent value="novo">
<Card className="p-4">
{canCreate() ? (
<Button onClick={() => setShowCronogramaForm(true)}>
Novo Cronograma
</Button>
) : (
<div className="text-center text-muted-foreground">
Você não tem permissão para criar cronogramas
</div>
)}
</Card>
</TabsContent>
</Tabs>
{isMobile && canCreate() && (
<div className="fixed bottom-4 right-4 space-y-2">
<Button
size="sm"
className="w-full"
onClick={() => setShowCronogramaForm(true)}
>
Novo Cronograma
</Button>
</div>
)}
{canCreate() && (
<CronogramaForm
cronograma={selectedCronograma}
onClose={handleCloseCronogramaForm}
isOpen={showCronogramaForm}
/>
)}
{showGanttChart && cronogramaForGantt && (
<CronogramaGantt
cronograma={cronogramaForGantt}
onClose={handleCloseGanttChart}
/>
)}
{showPDFGenerator && cronogramaForPDF && (
<CronogramaPDF
cronograma={cronogramaForPDF}
onComplete={handleClosePDFGenerator}
/>
)}
</div>
);
};
export default CronogramaOF;
+137
View File
@@ -0,0 +1,137 @@
import React from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { CalendarClock } from '@/components/dashboard/CalendarClock';
import { UserInfo } from '@/components/dashboard/UserInfo';
import { TaskTypeChart } from '@/components/dashboard/TaskTypeChart';
import { TaskStatusChart } from '@/components/dashboard/TaskStatusChart';
import { OnlineUsers } from '@/components/dashboard/OnlineUsers';
import NotificationsSugestoes from '@/components/dashboard/NotificationsSugestoes';
import { BeamsBackground } from '@/components/ui/beams-background';
import { Workflow, BarChart3, Settings, FileText, Clock, CheckCircle } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { cn } from '@/lib/utils';
export default function Dashboard() {
const navigate = useNavigate();
return (
<BeamsBackground intensity="subtle">
<div className="w-full min-h-screen">
<div className="container mx-auto p-4 space-y-6">
{/* Header com informações do usuário e botão Fluxo do Sistema */}
<div className="flex flex-col lg:flex-row justify-between items-start lg:items-center gap-4">
<div>
<h1 className="text-3xl font-bold text-foreground">Dashboard</h1>
<p className="text-muted-foreground">Painel de controle e monitoramento avançado</p>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-4">
{/* Card de usuários online */}
<div className="w-full sm:w-auto">
<OnlineUsers />
</div>
{/* Botão Fluxo do Sistema */}
<Button
onClick={() => navigate('/mapa-interativo')}
className="bg-blue-100 hover:bg-blue-200 text-gray-800 dark:bg-blue-600 dark:hover:bg-blue-700 dark:text-white gap-2 group relative"
size="lg"
>
<Workflow className="w-5 h-5" />
Fluxo do Sistema
{/* Tooltip */}
<div className="absolute bottom-full mb-2 right-0 px-3 py-2 bg-popover text-popover-foreground text-sm rounded-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none z-50 w-48 text-left border border-border shadow-md">
Use o mapa interativo para
<br />
entender o fluxo do
<br />
sistema
<div className="absolute top-full right-6 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-popover"></div>
</div>
</Button>
</div>
</div>
{/* Grid principal */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Coluna principal esquerda */}
<div className="lg:col-span-3 space-y-6">
{/* Cards de acesso rápido */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
<Button
variant="outline"
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-green-50 dark:hover:bg-green-950 border-green-200 dark:border-green-800"
onClick={() => navigate('/cadastro-of')}
>
<Settings className="w-6 h-6 text-green-600" />
<span className="text-xs text-center">Ficha Técnica da OF</span>
</Button>
<Button
variant="outline"
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-blue-50 dark:hover:bg-blue-950 border-blue-200 dark:border-blue-800"
onClick={() => navigate('/ofs')}
>
<FileText className="w-6 h-6 text-blue-600" />
<span className="text-xs text-center">Painel das OFs</span>
</Button>
<Button
variant="outline"
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-purple-50 dark:hover:bg-purple-950 border-purple-200 dark:border-purple-800"
onClick={() => navigate('/painel-industrial')}
>
<BarChart3 className="w-6 h-6 text-purple-600" />
<span className="text-xs text-center">Painel Industrial</span>
</Button>
<Button
variant="outline"
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-orange-50 dark:hover:bg-orange-950 border-orange-200 dark:border-orange-800"
onClick={() => navigate('/obra')}
>
<Settings className="w-6 h-6 text-orange-600" />
<span className="text-xs text-center">Painel de Obras</span>
</Button>
<Button
variant="outline"
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-yellow-50 dark:hover:bg-yellow-950 border-yellow-200 dark:border-yellow-800"
onClick={() => navigate('/estoque')}
>
<Settings className="w-6 h-6 text-yellow-600" />
<span className="text-xs text-center">Estoque</span>
</Button>
<Button
variant="outline"
className="h-20 flex flex-col items-center justify-center gap-2 hover:bg-red-50 dark:hover:bg-red-950 border-red-200 dark:border-red-800"
onClick={() => navigate('/expedicao')}
>
<Settings className="w-6 h-6 text-red-600" />
<span className="text-xs text-center">Expedição</span>
</Button>
</div>
{/* Gráficos de tarefas */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<TaskTypeChart />
<TaskStatusChart />
</div>
</div>
{/* Sidebar direita */}
<div className="space-y-6">
<UserInfo />
<CalendarClock />
<NotificationsSugestoes />
</div>
</div>
</div>
</div>
</BeamsBackground>
);
}
+156
View File
@@ -0,0 +1,156 @@
import React, { useState } from 'react';
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
import { StandardCard } from '@/components/layout/StandardCard';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { BarChart3, TrendingUp, Activity, RefreshCw, Clock } from 'lucide-react';
import { ResumoOF } from '@/components/dashboard-producao/ResumoOF';
import { GraficoMestre } from '@/components/dashboard-producao/GraficoMestre';
import { GraficoProgressoIndividual } from '@/components/dashboard-producao/GraficoProgressoIndividual';
import { TabelaResumoProcessos } from '@/components/dashboard-producao/TabelaResumoProcessos';
import { useDashboardProducaoOtimizado } from '@/hooks/useDashboardProducaoOtimizado';
import { useOFs } from '@/hooks/useOFs';
import { useIsMobile } from '@/hooks/use-mobile';
const DashboardProducao = () => {
const [selectedOF, setSelectedOF] = useState<string>('');
const [activeTab, setActiveTab] = useState('geral');
const isMobile = useIsMobile();
// Converter o valor selecionado para o hook (vazio se for "all")
const ofForDashboard = selectedOF === 'all' ? '' : selectedOF;
const { dashboardData, loading, refetch } = useDashboardProducaoOtimizado(ofForDashboard);
const { ofs, loading: ofsLoading } = useOFs();
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<StandardPageLayout
title="Dashboard de Produção"
subtitle="Acompanhamento em tempo real da produção"
badge={{
text: selectedOF && selectedOF !== 'all' ? `OF: ${selectedOF}` : 'Visão Geral',
variant: 'secondary'
}}
actions={
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
<Select value={selectedOF} onValueChange={setSelectedOF}>
<SelectTrigger className="w-full sm:w-[200px] text-xs md:text-sm">
<SelectValue placeholder={ofsLoading ? "Carregando..." : "Selecionar OF"} />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas as OFs</SelectItem>
{ofs.map((of) => (
<SelectItem key={of.id} value={of.num_of}>
OF {of.num_of} {of.descritivo ? `- ${of.descritivo}` : ''}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
onClick={refetch}
variant="outline"
className="mobile-full-width text-xs md:text-sm"
disabled={loading}
>
<RefreshCw className={`h-3 w-3 md:h-4 md:w-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Atualizar
</Button>
</div>
}
>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className={`w-full bg-muted h-auto p-1 ${
isMobile
? 'grid grid-cols-1 gap-1'
: 'grid grid-cols-3'
}`}>
<TabsTrigger
value="geral"
className={`flex items-center gap-1 md:gap-2 text-xs md:text-sm ${
isMobile ? 'w-full justify-start p-3' : 'p-2 md:p-3'
}`}
>
<BarChart3 className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Visão Geral</span>
<span className="sm:hidden">Geral</span>
</TabsTrigger>
<TabsTrigger
value="grafico"
className={`flex items-center gap-1 md:gap-2 text-xs md:text-sm ${
isMobile ? 'w-full justify-start p-3' : 'p-2 md:p-3'
}`}
>
<TrendingUp className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Gráficos</span>
<span className="sm:hidden">Gráfico</span>
</TabsTrigger>
<TabsTrigger
value="progresso"
className={`flex items-center gap-1 md:gap-2 text-xs md:text-sm ${
isMobile ? 'w-full justify-start p-3' : 'p-2 md:p-3'
}`}
>
<Clock className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Progresso</span>
<span className="sm:hidden">Prog</span>
</TabsTrigger>
</TabsList>
<TabsContent value="geral" className="space-y-4 mt-4">
<div className="dashboard-grid">
<ResumoOF
of={ofForDashboard}
data={dashboardData}
loading={loading}
/>
</div>
<StandardCard title="Resumo dos Processos" icon={Activity}>
<div className="overflow-x-auto custom-scrollbar">
<TabelaResumoProcessos
data={dashboardData}
loading={loading}
/>
</div>
</StandardCard>
</TabsContent>
<TabsContent value="grafico" className="space-y-4 mt-4">
<StandardCard title="Gráfico de Produção" icon={TrendingUp}>
<div className="dashboard-card">
{dashboardData?.processos && dashboardData.processos.length > 0 ? (
<GraficoMestre
processos={dashboardData.processos}
onProcessoClick={(processoNome) => console.log('Processo selecionado:', processoNome)}
processoSelecionado={null}
/>
) : (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>Selecione uma OF para visualizar o gráfico de produção</p>
</div>
)}
</div>
</StandardCard>
</TabsContent>
<TabsContent value="progresso" className="space-y-4 mt-4">
<StandardCard title="Progresso por Processo" icon={Clock}>
<div className="dashboard-card">
{dashboardData?.processos && dashboardData.processos.length > 0 ? (
<GraficoProgressoIndividual processos={dashboardData.processos} />
) : (
<div className="flex items-center justify-center h-96 text-muted-foreground">
<p>Selecione uma OF para visualizar o progresso dos processos</p>
</div>
)}
</div>
</StandardCard>
</TabsContent>
</Tabs>
</StandardPageLayout>
</div>
);
};
export default DashboardProducao;
+650
View File
@@ -0,0 +1,650 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Checkbox } from '@/components/ui/checkbox';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Badge } from '@/components/ui/badge';
import { Plus, Save, X, Camera, Upload, FileText, Trash2, Edit, Eye, Printer } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useDiarioProducao, type RecursoProducao, type ApontamentoDiarioRecurso, type LoteSoldaDiario } from '@/hooks/useDiarioProducao';
import { useUserProfile } from '@/hooks/useUserProfile';
import { useFichaTecnica } from '@/hooks/useFichaTecnica';
interface DiarioData {
id?: string;
data: string;
tecnico: string;
turno: string;
ofs: string[];
apontamentos: { [ofId: string]: { [recursoId: string]: { inicio: number; meio: number; fim: number; segundo: number } } };
lotesSolda: { [ofId: string]: string };
ocorrencias: string[];
observacoes: string;
fotos: File[];
}
const DiarioProducao = () => {
const [activeTab, setActiveTab] = useState('diario');
const [selectedOF, setSelectedOF] = useState('');
const [showOFModal, setShowOFModal] = useState(false);
const [viewDiario, setViewDiario] = useState(null);
const { profile: userProfile } = useUserProfile();
const { fichasTecnicas, isLoadingFichas } = useFichaTecnica();
const {
diarios,
recursos,
ocorrencias,
isLoadingRecursos,
isLoadingOcorrencias,
salvarDiario,
salvarApontamentos,
salvarLotesSolda,
salvarOcorrenciasDiario,
deletarDiario
} = useDiarioProducao();
const [currentDiario, setCurrentDiario] = useState<DiarioData>({
data: new Date().toISOString().split('T')[0],
tecnico: userProfile?.full_name || 'Técnico',
turno: '',
ofs: [],
apontamentos: {},
lotesSolda: {},
ocorrencias: [],
observacoes: '',
fotos: []
});
// Atualizar nome do técnico quando perfil carrega
useEffect(() => {
if (userProfile?.full_name) {
setCurrentDiario(prev => ({
...prev,
tecnico: userProfile.full_name
}));
}
}, [userProfile]);
const adicionarOF = () => {
if (selectedOF && !currentDiario.ofs.includes(selectedOF)) {
setCurrentDiario(prev => {
const newApontamentos = { ...prev.apontamentos };
const newLotesSolda = { ...prev.lotesSolda };
if (recursos) {
newApontamentos[selectedOF] = recursos.reduce((acc, recurso) => ({
...acc,
[recurso.id]: { inicio: 0, meio: 0, fim: 0, segundo: 0 }
}), {});
}
newLotesSolda[selectedOF] = '';
return {
...prev,
ofs: [...prev.ofs, selectedOF],
apontamentos: newApontamentos,
lotesSolda: newLotesSolda
};
});
setSelectedOF('');
setShowOFModal(false);
}
};
const removerOF = (ofId: string) => {
setCurrentDiario(prev => {
const newApontamentos = { ...prev.apontamentos };
const newLotesSolda = { ...prev.lotesSolda };
delete newApontamentos[ofId];
delete newLotesSolda[ofId];
return {
...prev,
ofs: prev.ofs.filter(id => id !== ofId),
apontamentos: newApontamentos,
lotesSolda: newLotesSolda
};
});
};
const atualizarApontamento = (ofId: string, recursoId: string, periodo: 'inicio' | 'meio' | 'fim' | 'segundo', valor: number) => {
setCurrentDiario(prev => ({
...prev,
apontamentos: {
...prev.apontamentos,
[ofId]: {
...prev.apontamentos[ofId],
[recursoId]: {
...prev.apontamentos[ofId][recursoId],
[periodo]: valor
}
}
}
}));
};
const salvarDiarioCompleto = async () => {
try {
// 1. Salvar o diário principal
const diarioSalvo = await salvarDiario.mutateAsync({
data: currentDiario.data,
tecnico_responsavel: currentDiario.tecnico,
turno: currentDiario.turno,
observacoes_gerais: currentDiario.observacoes,
fotos_urls: [], // TODO: implementar upload de fotos
finalizado: false
});
if (diarioSalvo?.id) {
// 2. Salvar apontamentos de recursos
const apontamentosParaSalvar: ApontamentoDiarioRecurso[] = [];
Object.entries(currentDiario.apontamentos).forEach(([ofId, recursosData]) => {
Object.entries(recursosData).forEach(([recursoId, quantidades]) => {
apontamentosParaSalvar.push({
diario_id: diarioSalvo.id,
of_number: ofId,
recurso_id: recursoId,
qtd_inicio: quantidades.inicio,
qtd_meio: quantidades.meio,
qtd_fim: quantidades.fim,
qtd_segundo_turno: quantidades.segundo
});
});
});
if (apontamentosParaSalvar.length > 0) {
await salvarApontamentos.mutateAsync(apontamentosParaSalvar);
}
// 3. Salvar lotes de solda
const lotesParaSalvar: LoteSoldaDiario[] = Object.entries(currentDiario.lotesSolda)
.filter(([, lote]) => lote.trim() !== '')
.map(([ofId, lote]) => ({
diario_id: diarioSalvo.id,
of_number: ofId,
lote_solda: lote
}));
if (lotesParaSalvar.length > 0) {
await salvarLotesSolda.mutateAsync(lotesParaSalvar);
}
// 4. Salvar ocorrências
if (currentDiario.ocorrencias.length > 0 && ocorrencias) {
const ocorrenciaIds = ocorrencias
.filter(oc => currentDiario.ocorrencias.includes(oc.descricao))
.map(oc => oc.id);
if (ocorrenciaIds.length > 0) {
await salvarOcorrenciasDiario.mutateAsync({
diario_id: diarioSalvo.id,
ocorrencia_ids: ocorrenciaIds
});
}
}
// Limpar formulário
setCurrentDiario({
data: new Date().toISOString().split('T')[0],
tecnico: userProfile?.full_name || 'Técnico',
turno: '',
ofs: [],
apontamentos: {},
lotesSolda: {},
ocorrencias: [],
observacoes: '',
fotos: []
});
setActiveTab('historico');
}
} catch (error) {
console.error('Erro ao salvar diário completo:', error);
}
};
const getFichaTecnicaByOF = (ofNumber: string) => {
return fichasTecnicas?.find(ficha => ficha.of_number === ofNumber);
};
const getOFDisplayName = (ofNumber: string) => {
const ficha = getFichaTecnicaByOF(ofNumber);
return ficha ? `${ofNumber}: ${ficha.descricao_resumida || 'Sem descrição'}` : ofNumber;
};
// Filtrar OFs disponíveis que não foram ainda adicionadas
const ofsDisponiveis = fichasTecnicas?.filter(ficha =>
ficha.of_number && !currentDiario.ofs.includes(ficha.of_number)
) || [];
if (isLoadingRecursos || isLoadingOcorrencias || isLoadingFichas) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p>Carregando dados...</p>
</div>
</div>
);
}
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground mb-2">Diário de Produção</h1>
<p className="text-sm md:text-base text-muted-foreground">Registro de uso de recursos em Ordens de Fabricação</p>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="diario">Novo Diário</TabsTrigger>
<TabsTrigger value="historico">Histórico</TabsTrigger>
<TabsTrigger value="dashboard">Dashboard</TabsTrigger>
</TabsList>
{/* ABA NOVO DIÁRIO */}
<TabsContent value="diario" className="space-y-6">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl text-foreground">Informações Gerais</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<Label htmlFor="data">Data</Label>
<Input
id="data"
type="date"
value={currentDiario.data}
onChange={(e) => setCurrentDiario(prev => ({ ...prev, data: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="tecnico">Técnico Responsável</Label>
<Input
id="tecnico"
value={currentDiario.tecnico}
onChange={(e) => setCurrentDiario(prev => ({ ...prev, tecnico: e.target.value }))}
/>
</div>
<div>
<Label htmlFor="turno">Turno</Label>
<Select value={currentDiario.turno} onValueChange={(value) => setCurrentDiario(prev => ({ ...prev, turno: value }))}>
<SelectTrigger>
<SelectValue placeholder="Selecione o turno" />
</SelectTrigger>
<SelectContent>
<SelectItem value="1-manha">1º Turno - Manhã</SelectItem>
<SelectItem value="1-tarde">1º Turno - Tarde</SelectItem>
<SelectItem value="2-turno">2º Turno</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* APONTAMENTO POR OF */}
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl text-foreground flex items-center justify-between">
Apontamento de Recursos por OF
<Dialog open={showOFModal} onOpenChange={setShowOFModal}>
<DialogTrigger asChild>
<Button size="sm" className="flex items-center gap-2">
<Plus className="h-4 w-4" />
Adicionar OF
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Selecionar OF</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{ofsDisponiveis.length === 0 ? (
<p className="text-center text-muted-foreground py-4">
Nenhuma OF disponível para seleção
</p>
) : (
<Select value={selectedOF} onValueChange={setSelectedOF}>
<SelectTrigger>
<SelectValue placeholder="Selecione uma OF" />
</SelectTrigger>
<SelectContent>
{ofsDisponiveis.map(ficha => (
<SelectItem key={ficha.of_number} value={ficha.of_number}>
{getOFDisplayName(ficha.of_number)}
</SelectItem>
))}
</SelectContent>
</Select>
)}
<div className="flex gap-2">
<Button
onClick={adicionarOF}
className="flex-1"
disabled={!selectedOF || ofsDisponiveis.length === 0}
>
Adicionar
</Button>
<Button variant="outline" onClick={() => setShowOFModal(false)} className="flex-1">Cancelar</Button>
</div>
</div>
</DialogContent>
</Dialog>
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile space-y-6">
{currentDiario.ofs.length === 0 ? (
<p className="text-center text-muted-foreground py-8">Nenhuma OF adicionada. Clique em "Adicionar OF" para começar.</p>
) : (
currentDiario.ofs.map(ofId => (
<div key={ofId} className="border rounded-lg p-4 space-y-4">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-lg">
<Badge variant="outline" className="mr-2">{ofId}</Badge>
{getFichaTecnicaByOF(ofId)?.descricao_resumida || 'Sem descrição'}
</h3>
<Button variant="ghost" size="sm" onClick={() => removerOF(ofId)}>
<X className="h-4 w-4" />
</Button>
</div>
{/* Tabela de Recursos */}
<div className="overflow-x-auto">
<table className="w-full border-collapse border border-gray-300 text-sm">
<thead>
<tr className="bg-muted">
<th className="border border-gray-300 p-2 text-left">Recurso</th>
<th className="border border-gray-300 p-2 text-center">Início</th>
<th className="border border-gray-300 p-2 text-center">Meio-dia</th>
<th className="border border-gray-300 p-2 text-center">Fim</th>
<th className="border border-gray-300 p-2 text-center">2º Turno</th>
</tr>
</thead>
<tbody>
{/* Máquinas */}
<tr>
<td colSpan={5} className="border border-gray-300 p-2 bg-slate-100 font-semibold">Máquinas</td>
</tr>
{recursos?.filter(r => r.tipo === 'maquina').map(recurso => (
<tr key={recurso.id}>
<td className="border border-gray-300 p-2">{recurso.nome}</td>
<td className="border border-gray-300 p-1">
<Input
type="number"
min="0"
value={currentDiario.apontamentos[ofId]?.[recurso.id]?.inicio || 0}
onChange={(e) => atualizarApontamento(ofId, recurso.id, 'inicio', parseInt(e.target.value) || 0)}
className="w-full h-8"
/>
</td>
<td className="border border-gray-300 p-1">
<Input
type="number"
min="0"
value={currentDiario.apontamentos[ofId]?.[recurso.id]?.meio || 0}
onChange={(e) => atualizarApontamento(ofId, recurso.id, 'meio', parseInt(e.target.value) || 0)}
className="w-full h-8"
/>
</td>
<td className="border border-gray-300 p-1">
<Input
type="number"
min="0"
value={currentDiario.apontamentos[ofId]?.[recurso.id]?.fim || 0}
onChange={(e) => atualizarApontamento(ofId, recurso.id, 'fim', parseInt(e.target.value) || 0)}
className="w-full h-8"
/>
</td>
<td className="border border-gray-300 p-1">
<Input
type="number"
min="0"
value={currentDiario.apontamentos[ofId]?.[recurso.id]?.segundo || 0}
onChange={(e) => atualizarApontamento(ofId, recurso.id, 'segundo', parseInt(e.target.value) || 0)}
className="w-full h-8"
/>
</td>
</tr>
))}
{/* Operários */}
<tr>
<td colSpan={5} className="border border-gray-300 p-2 bg-slate-100 font-semibold">Operários</td>
</tr>
{recursos?.filter(r => r.tipo === 'operario').map(recurso => (
<tr key={recurso.id}>
<td className="border border-gray-300 p-2">{recurso.nome}</td>
<td className="border border-gray-300 p-1">
<Input
type="number"
min="0"
value={currentDiario.apontamentos[ofId]?.[recurso.id]?.inicio || 0}
onChange={(e) => atualizarApontamento(ofId, recurso.id, 'inicio', parseInt(e.target.value) || 0)}
className="w-full h-8"
/>
</td>
<td className="border border-gray-300 p-1">
<Input
type="number"
min="0"
value={currentDiario.apontamentos[ofId]?.[recurso.id]?.meio || 0}
onChange={(e) => atualizarApontamento(ofId, recurso.id, 'meio', parseInt(e.target.value) || 0)}
className="w-full h-8"
/>
</td>
<td className="border border-gray-300 p-1">
<Input
type="number"
min="0"
value={currentDiario.apontamentos[ofId]?.[recurso.id]?.fim || 0}
onChange={(e) => atualizarApontamento(ofId, recurso.id, 'fim', parseInt(e.target.value) || 0)}
className="w-full h-8"
/>
</td>
<td className="border border-gray-300 p-1">
<Input
type="number"
min="0"
value={currentDiario.apontamentos[ofId]?.[recurso.id]?.segundo || 0}
onChange={(e) => atualizarApontamento(ofId, recurso.id, 'segundo', parseInt(e.target.value) || 0)}
className="w-full h-8"
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Lote de Solda */}
<div>
<Label htmlFor={`lote-${ofId}`}>Lote de Insumo de Solda Utilizado</Label>
<Input
id={`lote-${ofId}`}
value={currentDiario.lotesSolda[ofId] || ''}
onChange={(e) => setCurrentDiario(prev => ({
...prev,
lotesSolda: { ...prev.lotesSolda, [ofId]: e.target.value }
}))}
placeholder="Ex: LOTE-SOLDA-XYZ-001"
/>
</div>
</div>
))
)}
</CardContent>
</Card>
{/* OCORRÊNCIAS E OBSERVAÇÕES */}
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl text-foreground">Ocorrências e Observações Gerais</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile space-y-6">
<div>
<Label className="text-base font-medium mb-3 block">Ocorrências de Improdutividade</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{ocorrencias?.map((ocorrencia) => (
<div key={ocorrencia.id} className="flex items-center space-x-2">
<Checkbox
id={`ocorrencia-${ocorrencia.id}`}
checked={currentDiario.ocorrencias.includes(ocorrencia.descricao)}
onCheckedChange={(checked) => {
if (checked) {
setCurrentDiario(prev => ({
...prev,
ocorrencias: [...prev.ocorrencias, ocorrencia.descricao]
}));
} else {
setCurrentDiario(prev => ({
...prev,
ocorrencias: prev.ocorrencias.filter(o => o !== ocorrencia.descricao)
}));
}
}}
/>
<Label htmlFor={`ocorrencia-${ocorrencia.id}`} className="text-sm">{ocorrencia.descricao}</Label>
</div>
))}
</div>
</div>
<div>
<Label htmlFor="observacoes">Observações Gerais</Label>
<Textarea
id="observacoes"
value={currentDiario.observacoes}
onChange={(e) => setCurrentDiario(prev => ({ ...prev, observacoes: e.target.value }))}
placeholder="Descreva qualquer situação não prevista, problemas encontrados ou informações relevantes sobre o dia de produção..."
rows={4}
/>
</div>
<div>
<Label className="text-base font-medium mb-3 block">Anexar Fotos</Label>
<div className="flex gap-3">
<Button variant="outline" className="flex items-center gap-2">
<Upload className="h-4 w-4" />
Upload de Arquivo
</Button>
<Button variant="outline" className="flex items-center gap-2">
<Camera className="h-4 w-4" />
Tirar Foto
</Button>
</div>
{currentDiario.fotos.length > 0 && (
<div className="mt-3 text-sm text-muted-foreground">
{currentDiario.fotos.length} foto(s) anexada(s)
</div>
)}
</div>
<div className="flex gap-3 pt-4 border-t">
<Button
onClick={salvarDiarioCompleto}
className="flex items-center gap-2"
disabled={salvarDiario.isPending}
>
<Save className="h-4 w-4" />
{salvarDiario.isPending ? 'Salvando...' : 'Salvar Diário'}
</Button>
<Button variant="outline" onClick={() => setCurrentDiario({
data: new Date().toISOString().split('T')[0],
tecnico: userProfile?.full_name || 'Técnico',
turno: '',
ofs: [],
apontamentos: {},
lotesSolda: {},
ocorrencias: [],
observacoes: '',
fotos: []
})}>
Cancelar
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
{/* ABA HISTÓRICO */}
<TabsContent value="historico" className="space-y-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl text-foreground">Histórico de Diários</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
{!diarios || diarios.length === 0 ? (
<p className="text-center text-muted-foreground py-8">Nenhum diário salvo ainda.</p>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left p-2">Data</th>
<th className="text-left p-2">Turno</th>
<th className="text-left p-2">Técnico</th>
<th className="text-center p-2">Ações</th>
</tr>
</thead>
<tbody>
{diarios.map((diario) => (
<tr key={diario.id} className="border-b">
<td className="p-2">{new Date(diario.data).toLocaleDateString('pt-BR')}</td>
<td className="p-2">{diario.turno}</td>
<td className="p-2">{diario.tecnico_responsavel}</td>
<td className="p-2">
<div className="flex gap-1 justify-center">
<Button variant="ghost" size="sm">
<Eye className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm">
<Printer className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => diario.id && deletarDiario.mutate(diario.id)}
disabled={deletarDiario.isPending}
>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</TabsContent>
{/* ABA DASHBOARD */}
<TabsContent value="dashboard" className="space-y-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl text-foreground">Dashboard e Relatórios</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<p className="text-center text-muted-foreground py-8">Dashboard em desenvolvimento...</p>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export default DiarioProducao;
+192
View File
@@ -0,0 +1,192 @@
import { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Search, Plus, Filter, Download, Upload } from 'lucide-react';
import { EquipamentosTable } from '@/components/equipamentos/EquipamentosTable';
import { EquipamentosFilters } from '@/components/equipamentos/EquipamentosFilters';
import { EquipamentosStats } from '@/components/equipamentos/EquipamentosStats';
import { EquipamentoModal } from '@/components/equipamentos/EquipamentoModal';
import { useEquipamentos } from '@/hooks/useEquipamentos';
import { useUserRole } from '@/hooks/useUserRole';
import { usePermissionControl } from '@/hooks/usePermissionControl';
import { EquipamentoLoanControl } from '@/components/equipamentos/EquipamentoLoanControl';
const Equipamentos = () => {
const { canCreate } = usePermissionControl();
const { isAdmin } = useUserRole();
const [searchTerm, setSearchTerm] = useState('');
const [showFilters, setShowFilters] = useState(false);
const [showModal, setShowModal] = useState(false);
const [editingEquipamento, setEditingEquipamento] = useState(null);
const {
equipamentos,
isLoading,
loading,
filters,
updateFilters,
clearFilters,
createEquipamento,
updateEquipamento,
deleteEquipamento,
stats
} = useEquipamentos();
const filteredEquipamentos = equipamentos.filter(equip =>
equip.codigo.toLowerCase().includes(searchTerm.toLowerCase()) ||
equip.descricao.toLowerCase().includes(searchTerm.toLowerCase())
);
const handleEdit = (equipamento: any) => {
setEditingEquipamento(equipamento);
setShowModal(true);
};
const handleCloseModal = () => {
setShowModal(false);
setEditingEquipamento(null);
};
const handleSave = async (data: any) => {
try {
if (editingEquipamento) {
await updateEquipamento.mutateAsync({ id: editingEquipamento.id, data });
} else {
await createEquipamento.mutateAsync(data);
}
handleCloseModal();
} catch (error) {
console.error('Erro ao salvar equipamento:', error);
}
};
const handleDelete = (id: string) => {
deleteEquipamento.mutate(id);
};
const [loanControlOpen, setLoanControlOpen] = useState(false);
const [selectedEquipamento, setSelectedEquipamento] = useState<any | null>(null);
const handleLoanControl = (equipamento: any) => {
setSelectedEquipamento(equipamento);
setLoanControlOpen(true);
};
if (isLoading || loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="space-y-6 p-4 md:p-6">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-slate-900 dark:text-white">Equipamentos</h1>
<p className="text-sm md:text-base text-slate-600 dark:text-slate-400">Gerencie equipamentos e máquinas</p>
</div>
{canCreate && (
<Button
onClick={() => setShowModal(true)}
className="bg-green-600 hover:bg-green-700 w-full sm:w-auto"
>
<Plus className="w-4 h-4 mr-2" />
<span className="text-sm md:text-base">Novo Equipamento</span>
</Button>
)}
</div>
<EquipamentosStats stats={stats} />
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
<CardHeader className="space-y-4">
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
<CardTitle className="text-lg md:text-xl text-slate-900 dark:text-white">
Lista de Equipamentos
</CardTitle>
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1 sm:flex-initial">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
<Input
placeholder="Buscar equipamentos..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10 w-full sm:w-64"
/>
</div>
<Button
variant="outline"
onClick={() => setShowFilters(!showFilters)}
className="w-full sm:w-auto"
>
<Filter className="w-4 h-4 mr-2" />
<span className="text-sm">Filtros</span>
</Button>
{isAdmin && (
<>
<Button variant="outline" className="w-full sm:w-auto">
<Upload className="w-4 h-4 mr-2" />
<span className="text-sm">Importar</span>
</Button>
<Button variant="outline" className="w-full sm:w-auto">
<Download className="w-4 h-4 mr-2" />
<span className="text-sm">Exportar</span>
</Button>
</>
)}
</div>
</div>
{showFilters && (
<EquipamentosFilters
searchTerm={filters.search}
statusFilter={filters.status}
propriedadeFilter={filters.propriedade}
onSearchChange={(value) => updateFilters({ search: value })}
onStatusChange={(value) => updateFilters({ status: value })}
onPropriedadeChange={(value) => updateFilters({ propriedade: value })}
onResetFilters={clearFilters}
/>
)}
</CardHeader>
<CardContent className="p-0 md:p-6">
<EquipamentosTable
equipamentos={filteredEquipamentos}
onEdit={handleEdit}
onDelete={handleDelete}
onLoanControl={handleLoanControl}
/>
{/* Modal de controle de empréstimo */}
{selectedEquipamento && (
<EquipamentoLoanControl
isOpen={loanControlOpen}
onClose={() => {
setLoanControlOpen(false);
setSelectedEquipamento(null);
}}
equipamento={selectedEquipamento}
/>
)}
</CardContent>
</Card>
<EquipamentoModal
isOpen={showModal}
onClose={handleCloseModal}
equipamento={editingEquipamento}
onSave={handleSave}
/>
</div>
);
};
export default Equipamentos;
+197
View File
@@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
import { StandardCard } from '@/components/layout/StandardCard';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Warehouse, BarChart3, TrendingUp, Package, Download, Upload, AlertTriangle } from 'lucide-react';
import { EstoqueDashboard } from '@/components/estoque/EstoqueDashboard';
import { EstoqueTable } from '@/components/estoque/EstoqueTable';
import { MovimentacaoEstoqueSimplificada } from '@/components/estoque/MovimentacaoEstoqueSimplificada';
import { EstoqueReports } from '@/components/estoque/EstoqueReports';
import { EmpenhosMaterialSimplificado } from '@/components/estoque/EmpenhosMaterialSimplificado';
import { EstoqueCSVImportModal } from '@/components/estoque/EstoqueCSVImportModal';
import { useEstoque } from '@/hooks/useEstoqueSimplificado';
import { usePermissionControl } from '@/hooks/usePermissionControl';
import { generateCSV, downloadCSV } from '@/utils/csvUtils';
import { toast } from 'sonner';
const Estoque = () => {
const [activeTab, setActiveTab] = useState('estoque');
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const { canImportExport } = usePermissionControl();
const { materiais, loading } = useEstoque();
const handleExportCSV = () => {
if (materiais.length === 0) {
toast.error('Nenhum material disponível para exportar');
return;
}
const headers = [
'codigo',
'descricao',
'tipo_material',
'unidade',
'quantidade_total',
'quantidade_disponivel',
'quantidade_empenhada',
'quantidade_minima',
'quantidade_maxima',
'peso_unitario',
'valor_unitario',
'lote_atual',
'fornecedor',
'localizacao',
'status',
'observacoes'
];
const csvData = materiais.map(material => ({
codigo: material.codigo,
descricao: material.descricao,
tipo_material: material.tipos_materia_prima?.nome || '',
unidade: material.unidade,
quantidade_total: material.quantidade_total,
quantidade_disponivel: material.quantidade_disponivel,
quantidade_empenhada: material.quantidade_empenhada,
quantidade_minima: material.quantidade_minima,
quantidade_maxima: material.quantidade_maxima || '',
peso_unitario: material.peso_unitario,
valor_unitario: material.valor_unitario || '',
lote_atual: material.lote_atual || '',
fornecedor: material.fornecedor || '',
localizacao: material.localizacao || '',
status: material.status,
observacoes: material.observacoes || ''
}));
const csvContent = generateCSV(csvData, headers);
const fileName = `estoque_materiais_${new Date().toISOString().split('T')[0]}.csv`;
downloadCSV(csvContent, fileName);
toast.success(`${materiais.length} materiais exportados com sucesso!`);
};
const renderActions = () => {
const actions = [];
if (canImportExport()) {
actions.push(
<Button
key="export"
variant="outline"
className="bg-slate-700 border-slate-600 text-white hover:bg-slate-600"
onClick={handleExportCSV}
>
<Download className="h-4 w-4 mr-2" />
Exportar
</Button>
);
actions.push(
<Button
key="import"
variant="outline"
className="bg-slate-700 border-slate-600 text-white hover:bg-slate-600"
onClick={() => setIsImportModalOpen(true)}
>
<Upload className="h-4 w-4 mr-2" />
Importar
</Button>
);
}
return actions.length > 0 ? (
<div className="flex gap-2">
{actions}
</div>
) : null;
};
return (
<>
<StandardPageLayout
title="Controle de Estoque"
subtitle="Sistema simplificado - Movimentações controlam empenhos automaticamente"
badge={{
text: `${materiais.length} itens`,
variant: 'secondary'
}}
actions={renderActions()}
>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-5 bg-slate-800 border-slate-700">
<TabsTrigger
value="estoque"
className="flex items-center gap-2 text-blue-400 data-[state=active]:bg-slate-700 data-[state=active]:text-blue-300"
>
<Warehouse className="h-4 w-4" />
Estoque
</TabsTrigger>
<TabsTrigger
value="dashboard"
className="flex items-center gap-2 text-green-400 data-[state=active]:bg-slate-700 data-[state=active]:text-green-300"
>
<BarChart3 className="h-4 w-4" />
Dashboard
</TabsTrigger>
<TabsTrigger
value="movimentacao"
className="flex items-center gap-2 text-purple-400 data-[state=active]:bg-slate-700 data-[state=active]:text-purple-300"
>
<TrendingUp className="h-4 w-4" />
Movimentação
</TabsTrigger>
<TabsTrigger
value="empenhos"
className="flex items-center gap-2 text-yellow-400 data-[state=active]:bg-slate-700 data-[state=active]:text-yellow-300"
>
<AlertTriangle className="h-4 w-4" />
Empenhos
</TabsTrigger>
<TabsTrigger
value="relatorios"
className="flex items-center gap-2 text-orange-400 data-[state=active]:bg-slate-700 data-[state=active]:text-orange-300"
>
<Package className="h-4 w-4" />
Relatórios
</TabsTrigger>
</TabsList>
<TabsContent value="estoque" className="space-y-6">
<StandardCard title="Materiais em Estoque" icon={Warehouse}>
<EstoqueTable />
</StandardCard>
</TabsContent>
<TabsContent value="dashboard" className="space-y-6">
<EstoqueDashboard />
</TabsContent>
<TabsContent value="movimentacao" className="space-y-6">
<MovimentacaoEstoqueSimplificada />
</TabsContent>
<TabsContent value="empenhos" className="space-y-6">
<EmpenhosMaterialSimplificado />
</TabsContent>
<TabsContent value="relatorios" className="space-y-6">
<StandardCard title="Relatórios de Estoque" icon={Package}>
<EstoqueReports />
</StandardCard>
</TabsContent>
</Tabs>
</StandardPageLayout>
<EstoqueCSVImportModal
isOpen={isImportModalOpen}
onClose={() => setIsImportModalOpen(false)}
/>
</>
);
};
export default Estoque;
+197
View File
@@ -0,0 +1,197 @@
import React, { useState } from 'react';
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
import { StandardCard } from '@/components/layout/StandardCard';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Warehouse, BarChart3, TrendingUp, Package, Download, Upload, AlertTriangle } from 'lucide-react';
import { EstoqueDashboardCards } from '@/components/estoque/EstoqueDashboardCards';
import { EstoqueTable } from '@/components/estoque/EstoqueTable';
import { MovimentacaoEstoqueSimplificada } from '@/components/estoque/MovimentacaoEstoqueSimplificada';
import { EstoqueReports } from '@/components/estoque/EstoqueReports';
import { EmpenhosMaterialSimplificado } from '@/components/estoque/EmpenhosMaterialSimplificado';
import { EstoqueCSVImportModal } from '@/components/estoque/EstoqueCSVImportModal';
import { useEstoque } from '@/hooks/useEstoqueSimplificado';
import { usePermissionControl } from '@/hooks/usePermissionControl';
import { generateCSV, downloadCSV } from '@/utils/csvUtils';
import { toast } from 'sonner';
const EstoqueSimplificado = () => {
const [activeTab, setActiveTab] = useState('estoque');
const [isImportModalOpen, setIsImportModalOpen] = useState(false);
const { canImportExport } = usePermissionControl();
const { materiais, loading } = useEstoque();
const handleExportCSV = () => {
if (materiais.length === 0) {
toast.error('Nenhum material disponível para exportar');
return;
}
const headers = [
'codigo',
'descricao',
'tipo_material',
'unidade',
'quantidade_total',
'quantidade_disponivel',
'quantidade_empenhada',
'quantidade_minima',
'quantidade_maxima',
'peso_unitario',
'valor_unitario',
'lote_atual',
'fornecedor',
'localizacao',
'status',
'observacoes'
];
const csvData = materiais.map(material => ({
codigo: material.codigo,
descricao: material.descricao,
tipo_material: material.tipos_materia_prima?.nome || '',
unidade: material.unidade,
quantidade_total: material.quantidade_total,
quantidade_disponivel: material.quantidade_disponivel,
quantidade_empenhada: material.quantidade_empenhada,
quantidade_minima: material.quantidade_minima,
quantidade_maxima: material.quantidade_maxima || '',
peso_unitario: material.peso_unitario,
valor_unitario: material.valor_unitario || '',
lote_atual: material.lote_atual || '',
fornecedor: material.fornecedor || '',
localizacao: material.localizacao || '',
status: material.status,
observacoes: material.observacoes || ''
}));
const csvContent = generateCSV(csvData, headers);
const fileName = `estoque_materiais_${new Date().toISOString().split('T')[0]}.csv`;
downloadCSV(csvContent, fileName);
toast.success(`${materiais.length} materiais exportados com sucesso!`);
};
const renderActions = () => {
const actions = [];
if (canImportExport()) {
actions.push(
<Button
key="export"
variant="outline"
className="bg-slate-700 border-slate-600 text-white hover:bg-slate-600"
onClick={handleExportCSV}
>
<Download className="h-4 w-4 mr-2" />
Exportar
</Button>
);
actions.push(
<Button
key="import"
variant="outline"
className="bg-slate-700 border-slate-600 text-white hover:bg-slate-600"
onClick={() => setIsImportModalOpen(true)}
>
<Upload className="h-4 w-4 mr-2" />
Importar
</Button>
);
}
return actions.length > 0 ? (
<div className="flex gap-2">
{actions}
</div>
) : null;
};
return (
<>
<StandardPageLayout
title="Controle de Estoque"
subtitle="Gerenciamento simplificado de materiais e movimentações"
badge={{
text: `${materiais.length} itens`,
variant: 'secondary'
}}
actions={renderActions()}
>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-5 bg-slate-800 border-slate-700">
<TabsTrigger
value="estoque"
className="flex items-center gap-2 text-blue-400 data-[state=active]:bg-slate-700 data-[state=active]:text-blue-300"
>
<Warehouse className="h-4 w-4" />
Estoque
</TabsTrigger>
<TabsTrigger
value="dashboard"
className="flex items-center gap-2 text-green-400 data-[state=active]:bg-slate-700 data-[state=active]:text-green-300"
>
<BarChart3 className="h-4 w-4" />
Dashboard
</TabsTrigger>
<TabsTrigger
value="movimentacao"
className="flex items-center gap-2 text-purple-400 data-[state=active]:bg-slate-700 data-[state=active]:text-purple-300"
>
<TrendingUp className="h-4 w-4" />
Movimentação
</TabsTrigger>
<TabsTrigger
value="empenhos"
className="flex items-center gap-2 text-yellow-400 data-[state=active]:bg-slate-700 data-[state=active]:text-yellow-300"
>
<AlertTriangle className="h-4 w-4" />
Empenhos
</TabsTrigger>
<TabsTrigger
value="relatorios"
className="flex items-center gap-2 text-orange-400 data-[state=active]:bg-slate-700 data-[state=active]:text-orange-300"
>
<Package className="h-4 w-4" />
Relatórios
</TabsTrigger>
</TabsList>
<TabsContent value="estoque" className="space-y-6">
<StandardCard title="Materiais em Estoque" icon={Warehouse}>
<EstoqueTable />
</StandardCard>
</TabsContent>
<TabsContent value="dashboard" className="space-y-6">
<EstoqueDashboardCards materiais={materiais} />
</TabsContent>
<TabsContent value="movimentacao" className="space-y-6">
<MovimentacaoEstoqueSimplificada />
</TabsContent>
<TabsContent value="empenhos" className="space-y-6">
<EmpenhosMaterialSimplificado />
</TabsContent>
<TabsContent value="relatorios" className="space-y-6">
<StandardCard title="Relatórios de Estoque" icon={Package}>
<EstoqueReports />
</StandardCard>
</TabsContent>
</Tabs>
</StandardPageLayout>
<EstoqueCSVImportModal
isOpen={isImportModalOpen}
onClose={() => setIsImportModalOpen(false)}
/>
</>
);
};
export default EstoqueSimplificado;
+523
View File
@@ -0,0 +1,523 @@
import React, { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import {
Plus,
Search,
Filter,
Truck,
Package,
Calendar,
FileText,
Edit,
Eye,
Menu,
X
} from 'lucide-react';
import { RomaneioTable } from '@/components/expedicao/RomaneioTable';
import { RomaneioForm } from '@/components/expedicao/RomaneioForm';
import { ItensRomaneioModal } from '@/components/expedicao/ItensRomaneioModal';
import { useRomaneios, useCriarRomaneio, useAtualizarRomaneio } from '@/hooks/useRomaneios';
import { useRemoverRomaneio } from '@/hooks/useRemoverRomaneio';
import { useOFsAtivas } from '@/hooks/useOFsAtivas';
import { useMobileResponsive } from '@/hooks/useMobileResponsive';
import { RomaneioExpedicao } from '@/hooks/useRomaneios';
import { RelatoriosExpedicao } from '@/components/expedicao/RelatoriosExpedicao';
const Expedicao = () => {
const { isMobile } = useMobileResponsive();
const { data: romaneios, isLoading, refetch } = useRomaneios();
const { data: ofsAtivas, isLoading: isLoadingOFs } = useOFsAtivas();
const criarRomaneioMutation = useCriarRomaneio();
const atualizarRomaneioMutation = useAtualizarRomaneio();
const removerRomaneioMutation = useRemoverRomaneio();
const [activeTab, setActiveTab] = useState('romaneios');
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [pecaFilter, setPecaFilter] = useState('');
const [showMobileFilters, setShowMobileFilters] = useState(false);
const [showRomaneioForm, setShowRomaneioForm] = useState(false);
const [showItensModal, setShowItensModal] = useState(false);
const [selectedRomaneio, setSelectedRomaneio] = useState<RomaneioExpedicao | null>(null);
const [romaneioToDelete, setRomaneioToDelete] = useState<string | null>(null);
// Memorizar última OF visualizada
const [ofFilter, setOfFilter] = useState(() => {
const saved = localStorage.getItem('expedicao-last-of-filter');
return saved || 'all';
});
// Salvar OF selecionada no localStorage
useEffect(() => {
if (ofFilter !== 'all') {
localStorage.setItem('expedicao-last-of-filter', ofFilter);
}
}, [ofFilter]);
const filteredRomaneios = romaneios?.filter(romaneio => {
const matchesSearch = romaneio.numero_romaneio?.toLowerCase().includes(searchTerm.toLowerCase()) ||
romaneio.of_number?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || romaneio.status === statusFilter;
const matchesOF = ofFilter === 'all' || romaneio.of_number === ofFilter;
const matchesPeca = !pecaFilter || romaneio.itens_pecas?.some(item =>
item.marca?.toLowerCase().includes(pecaFilter.toLowerCase())
);
return matchesSearch && matchesStatus && matchesOF && matchesPeca;
});
const stats = {
total: filteredRomaneios?.length || 0,
planejamento: filteredRomaneios?.filter(r => r.status === 'Em planejamento').length || 0,
entregue: filteredRomaneios?.filter(r => r.status === 'Entregue').length || 0,
conferidoEmObra: filteredRomaneios?.filter(r => r.status === 'Conferido em Obra').length || 0,
};
// Calcular peso total dos romaneios filtrados
const totalPesoFiltrado = filteredRomaneios?.reduce((sum, romaneio) => sum + (romaneio.peso_total_romaneio || 0), 0) || 0;
const ofNumbers = ofsAtivas?.map(of => of.of_number) || [];
const uniqueOFs = [...new Set(romaneios?.map(r => r.of_number) || [])];
const handleEdit = (romaneio: RomaneioExpedicao) => {
setSelectedRomaneio(romaneio);
setShowRomaneioForm(true);
};
const handleView = (romaneio: RomaneioExpedicao) => {
setSelectedRomaneio(romaneio);
setShowItensModal(true);
};
const handleDelete = (romaneioId: string) => {
setRomaneioToDelete(romaneioId);
};
const confirmDelete = async () => {
if (romaneioToDelete) {
try {
await removerRomaneioMutation.mutateAsync(romaneioToDelete);
setRomaneioToDelete(null);
refetch();
} catch (error) {
console.error('Error deleting romaneio:', error);
}
}
};
// Função de print simplificada (removida a lógica de PDF)
const handlePrint = (romaneio: RomaneioExpedicao) => {
// A funcionalidade de impressão agora é tratada pelo RomaneioReportModal
console.log('Print function called for romaneio:', romaneio.numero_romaneio);
};
const handleSaveRomaneio = async (data: any) => {
try {
if (selectedRomaneio) {
// Atualizando romaneio existente
await atualizarRomaneioMutation.mutateAsync({
id: selectedRomaneio.id,
...data
});
} else {
// Criando novo romaneio
await criarRomaneioMutation.mutateAsync(data);
}
setShowRomaneioForm(false);
setSelectedRomaneio(null);
refetch();
} catch (error) {
console.error('Error saving romaneio:', error);
}
};
const handleCancelRomaneio = () => {
setShowRomaneioForm(false);
setSelectedRomaneio(null);
};
const handleCloseItensModal = () => {
setShowItensModal(false);
setSelectedRomaneio(null);
};
const MobileRomaneioCard = ({ romaneio }: { romaneio: any }) => (
<Card className="w-full mb-4 bg-card border-border">
<CardHeader className="pb-3">
<div className="flex justify-between items-start">
<CardTitle className="text-lg text-card-foreground">
#{romaneio.numero_romaneio}
</CardTitle>
<Badge variant="outline" className="text-xs">
{romaneio.status === 'Em planejamento' ? 'Em planejamento' :
romaneio.status === 'Confirmado' ? 'Entregue' :
romaneio.status === 'Conferido em Obra' ? 'Conferido em Obra' : romaneio.status}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="grid grid-cols-1 gap-2 text-sm">
<div className="flex items-center gap-2">
<Package className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">OF:</span>
<span className="text-card-foreground">{romaneio.of_number || 'N/A'}</span>
</div>
<div className="flex items-center gap-2">
<Truck className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">Transporte:</span>
<span className="text-card-foreground">{romaneio.tipo_transporte || 'N/A'}</span>
</div>
<div className="flex items-center gap-2">
<Calendar className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">Data:</span>
<span className="text-card-foreground">
{romaneio.data_criacao
? new Date(romaneio.data_criacao).toLocaleDateString('pt-BR')
: 'N/A'
}
</span>
</div>
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-muted-foreground" />
<span className="text-muted-foreground">Peso:</span>
<span className="text-card-foreground">{romaneio.peso_total_romaneio || 0} kg</span>
</div>
</div>
<div className="flex gap-2 pt-2 border-t border-border">
<Button size="sm" variant="outline" className="flex-1" onClick={() => handleView(romaneio)}>
<Eye className="w-4 h-4 mr-1" />
Ver
</Button>
<Button size="sm" variant="outline" className="flex-1" onClick={() => handleEdit(romaneio)}>
<Edit className="w-4 h-4 mr-1" />
Editar
</Button>
</div>
</CardContent>
</Card>
);
const MobileFilters = () => (
<div className="space-y-4 p-4">
<div>
<Label htmlFor="mobile-search" className="text-sm font-medium">
Buscar
</Label>
<div className="relative mt-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
id="mobile-search"
placeholder="Número, OF..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div>
<Label htmlFor="mobile-status" className="text-sm font-medium">
Status
</Label>
<select
id="mobile-status"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="mt-1 w-full px-3 py-2 border border-border rounded-md bg-background text-foreground"
>
<option value="all">Todos</option>
<option value="Em planejamento">Em planejamento</option>
<option value="Confirmado">Entregue</option>
<option value="Conferido em Obra">Conferido em Obra</option>
</select>
</div>
<div>
<Label htmlFor="mobile-of" className="text-sm font-medium">
OF
</Label>
<select
id="mobile-of"
value={ofFilter}
onChange={(e) => setOfFilter(e.target.value)}
className="mt-1 w-full px-3 py-2 border border-border rounded-md bg-background text-foreground"
>
<option value="all">Todas as OFs</option>
{uniqueOFs.map(ofNumber => (
<option key={ofNumber} value={ofNumber}>{ofNumber}</option>
))}
</select>
</div>
<div>
<Label htmlFor="mobile-peca" className="text-sm font-medium">
Buscar Peça
</Label>
<Input
id="mobile-peca"
placeholder="Marca da peça..."
value={pecaFilter}
onChange={(e) => setPecaFilter(e.target.value)}
/>
</div>
</div>
);
if (isLoading || isLoadingOFs) {
return (
<div className="p-6">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-muted rounded w-1/4"></div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="h-24 bg-muted rounded"></div>
))}
</div>
</div>
</div>
);
}
return (
<div className="p-3 sm:p-6 space-y-4 sm:space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">
Expedição
</h1>
<div className="flex items-center gap-2">
{isMobile && (
<Sheet open={showMobileFilters} onOpenChange={setShowMobileFilters}>
<SheetTrigger asChild>
<Button variant="outline" size="sm">
<Filter className="w-4 h-4 mr-2" />
Filtros
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-80">
<SheetHeader>
<SheetTitle>Filtros</SheetTitle>
<SheetDescription>
Filtre os romaneios por diversos critérios
</SheetDescription>
</SheetHeader>
<MobileFilters />
</SheetContent>
</Sheet>
)}
<Button size={isMobile ? "sm" : "default"} onClick={() => setShowRomaneioForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Novo Romaneio
</Button>
</div>
</div>
{/* Stats Cards */}
<div className={`grid gap-4 ${isMobile ? 'grid-cols-2' : 'grid-cols-2 md:grid-cols-4'}`}>
<Card className="bg-card border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total de Romaneios
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-card-foreground">{stats.total}</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Em planejamento
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-600">{stats.planejamento}</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Entregue
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-600">{stats.entregue}</div>
</CardContent>
</Card>
<Card className="bg-card border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Conferido em Obra
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-600">{stats.conferidoEmObra}</div>
</CardContent>
</Card>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className={`grid w-full ${isMobile ? 'grid-cols-2' : 'grid-cols-2'} mb-4`}>
<TabsTrigger value="romaneios" className="text-xs sm:text-sm">
Romaneios
</TabsTrigger>
<TabsTrigger value="relatorios" className="text-xs sm:text-sm">
Relatórios
</TabsTrigger>
</TabsList>
<TabsContent value="romaneios" className="space-y-4">
{/* Desktop Filters */}
{!isMobile && (
<Card className="p-4">
<div className="flex gap-4 items-center flex-wrap">
<div className="relative flex-1 max-w-sm">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-4 h-4" />
<Input
placeholder="Buscar romaneios..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
className="px-3 py-2 border border-border rounded-md bg-background text-foreground"
>
<option value="all">Todos os Status</option>
<option value="Em planejamento">Em planejamento</option>
<option value="Confirmado">Entregue</option>
<option value="Conferido em Obra">Conferido em Obra</option>
</select>
<select
value={ofFilter}
onChange={(e) => setOfFilter(e.target.value)}
className="px-3 py-2 border border-border rounded-md bg-background text-foreground"
>
<option value="all">Todas as OFs</option>
{uniqueOFs.map(ofNumber => (
<option key={ofNumber} value={ofNumber}>{ofNumber}</option>
))}
</select>
<Input
placeholder="Buscar peça..."
value={pecaFilter}
onChange={(e) => setPecaFilter(e.target.value)}
className="max-w-xs"
/>
</div>
</Card>
)}
{/* Content */}
{isMobile ? (
<div className="space-y-4">
{filteredRomaneios && filteredRomaneios.length > 0 ? (
filteredRomaneios.map((romaneio) => (
<MobileRomaneioCard
key={romaneio.id}
romaneio={romaneio}
/>
))
) : (
<Card className="p-8 text-center">
<p className="text-muted-foreground">Nenhum romaneio encontrado.</p>
</Card>
)}
</div>
) : (
<div className="space-y-4">
{/* Info header with counts and total weight */}
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold text-foreground">Romaneios de Expedição</h2>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>
{filteredRomaneios?.length || 0} {(filteredRomaneios?.length || 0) === 1 ? 'romaneio' : 'romaneios'}
</span>
<span className="font-medium">
Peso Total: {totalPesoFiltrado.toFixed(2)} kg
</span>
</div>
</div>
<RomaneioTable
romaneios={filteredRomaneios || []}
onEdit={handleEdit}
onView={handleView}
onDelete={handleDelete}
onPrint={handlePrint}
loading={isLoading}
/>
</div>
)}
</TabsContent>
<TabsContent value="relatorios">
<RelatoriosExpedicao />
</TabsContent>
</Tabs>
{/* Modals */}
<RomaneioForm
isOpen={showRomaneioForm}
onClose={handleCancelRomaneio}
romaneio={selectedRomaneio}
ofNumbers={ofNumbers}
onSave={handleSaveRomaneio}
onCancel={handleCancelRomaneio}
loading={criarRomaneioMutation.isPending || atualizarRomaneioMutation.isPending}
/>
{showItensModal && selectedRomaneio && (
<ItensRomaneioModal
romaneio={selectedRomaneio}
isOpen={showItensModal}
onClose={handleCloseItensModal}
/>
)}
{/* Delete Confirmation Dialog */}
<AlertDialog open={!!romaneioToDelete} onOpenChange={() => setRomaneioToDelete(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmar Exclusão</AlertDialogTitle>
<AlertDialogDescription>
Tem certeza que deseja excluir este romaneio? Esta ação não pode ser desfeita.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={confirmDelete} disabled={removerRomaneioMutation.isPending}>
{removerRomaneioMutation.isPending ? 'Excluindo...' : 'Excluir'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
};
export default Expedicao;
+24
View File
@@ -0,0 +1,24 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
const Grupos = () => {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Grupos e Equipe</h1>
<p className="text-slate-400">Gerenciamento de grupos e membros da equipe</p>
</div>
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white">Grupos Ativos</CardTitle>
</CardHeader>
<CardContent>
<p className="text-slate-400">Nenhum grupo encontrado.</p>
</CardContent>
</Card>
</div>
);
};
export default Grupos;
+633
View File
@@ -0,0 +1,633 @@
import React, { useState, useEffect, useRef } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import {
X,
Filter,
Eye,
Workflow,
Shield,
RotateCcw,
Edit,
Save
} from 'lucide-react';
import { useSystemMapAutoDiscovery, SystemMapNode, SystemMapConnection } from '@/hooks/useSystemMapAutoDiscovery';
import { useUserRole } from '@/hooks/useUserRole';
import { UserResourcePermissions } from '@/components/mapa-interativo/UserResourcePermissions';
import { cn } from '@/lib/utils';
export default function MapaInterativo() {
const { nodes, connections } = useSystemMapAutoDiscovery();
const { isAdmin } = useUserRole();
const [activeFilter, setActiveFilter] = useState<string>('all');
const [selectedNode, setSelectedNode] = useState<SystemMapNode | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [draggedNode, setDraggedNode] = useState<string | null>(null);
const [highlightedPath, setHighlightedPath] = useState<string[]>([]);
const [isEditing, setIsEditing] = useState(false);
const [editData, setEditData] = useState<{ title: string; description: string; group: string }>({ title: '', description: '', group: '' });
const mapContainerRef = useRef<HTMLDivElement>(null);
const filters = [
{ id: 'all', label: 'Ver Tudo', icon: Eye },
{ id: 'main-flow', label: 'Fluxo Principal', icon: Workflow },
{ id: 'permissions', label: 'Meus Acessos', icon: Shield }
];
const filteredNodes = nodes.filter(node => {
switch (activeFilter) {
case 'main-flow': return node.isMainFlow;
case 'permissions': return node.hasAccess;
default: return true;
}
});
const filteredConnections = connections.filter(conn => {
const sourceExists = filteredNodes.some(n => n.id === conn.from);
const targetExists = filteredNodes.some(n => n.id === conn.to);
return sourceExists && targetExists;
});
const getMapDimensions = () => {
if (filteredNodes.length === 0) return { width: 1200, height: 800 };
const cardWidth = 240;
const cardHeight = 140;
const padding = 100; // Padding extra para garantir espaço
let maxX = 0;
let maxY = 0;
filteredNodes.forEach(node => {
const nodeEl = mapContainerRef.current?.querySelector(`[data-node="${node.id}"]`) as HTMLElement;
if (nodeEl) {
const rect = nodeEl.getBoundingClientRect();
const containerRect = mapContainerRef.current!.getBoundingClientRect();
maxX = Math.max(maxX, rect.left - containerRect.left + cardWidth);
maxY = Math.max(maxY, rect.top - containerRect.top + cardHeight);
} else {
// Usar posições iniciais se o elemento ainda não foi renderizado
maxX = Math.max(maxX, node.left + cardWidth);
maxY = Math.max(maxY, node.top + cardHeight);
}
});
return {
width: Math.max(1200, maxX + padding),
height: Math.max(800, maxY + padding)
};
};
const [mapDimensions, setMapDimensions] = useState(getMapDimensions());
useEffect(() => {
const updateDimensions = () => {
setMapDimensions(getMapDimensions());
};
updateDimensions();
const interval = setInterval(updateDimensions, 1000); // Atualizar periodicamente
return () => clearInterval(interval);
}, [filteredNodes]);
const createCurvedPath = (fromX: number, fromY: number, toX: number, toY: number) => {
const midX = (fromX + toX) / 2;
const midY = (fromY + toY) / 2;
const dx = toX - fromX;
const dy = toY - fromY;
const distance = Math.sqrt(dx * dx + dy * dy);
const curvature = Math.min(distance * 0.3, 100);
const offsetX = -dy / distance * curvature;
const offsetY = dx / distance * curvature;
const controlX = midX + offsetX;
const controlY = midY + offsetY;
return `M ${fromX} ${fromY} Q ${controlX} ${controlY} ${toX} ${toY}`;
};
const getArrowPositions = (fromX: number, fromY: number, toX: number, toY: number) => {
const arrows = [];
const steps = 3; // Número de setas ao longo da linha
for (let i = 1; i <= steps; i++) {
const t = i / (steps + 1);
const midX = (fromX + toX) / 2;
const midY = (fromY + toY) / 2;
const dx = toX - fromX;
const dy = toY - fromY;
const distance = Math.sqrt(dx * dx + dy * dy);
const curvature = Math.min(distance * 0.3, 100);
const offsetX = -dy / distance * curvature;
const offsetY = dx / distance * curvature;
const controlX = midX + offsetX;
const controlY = midY + offsetY;
const x = (1 - t) * (1 - t) * fromX + 2 * (1 - t) * t * controlX + t * t * toX;
const y = (1 - t) * (1 - t) * fromY + 2 * (1 - t) * t * controlY + t * t * toY;
const tangentX = 2 * (1 - t) * (controlX - fromX) + 2 * t * (toX - controlX);
const tangentY = 2 * (1 - t) * (controlY - fromY) + 2 * t * (toY - controlY);
const angle = Math.atan2(tangentY, tangentX) * 180 / Math.PI;
arrows.push({ x, y, angle });
}
return arrows;
};
const updateConnections = () => {
if (!mapContainerRef.current) return;
const container = mapContainerRef.current;
const containerRect = container.getBoundingClientRect();
filteredConnections.forEach(conn => {
const fromEl = container.querySelector(`[data-node="${conn.from}"]`) as HTMLElement;
const toEl = container.querySelector(`[data-node="${conn.to}"]`) as HTMLElement;
if (!fromEl || !toEl) return;
const fromRect = fromEl.getBoundingClientRect();
const toRect = toEl.getBoundingClientRect();
const fromX = fromRect.left + fromRect.width / 2 - containerRect.left;
const fromY = fromRect.top + fromRect.height / 2 - containerRect.top;
const toX = toRect.left + toRect.width / 2 - containerRect.left;
const toY = toRect.top + toRect.height / 2 - containerRect.top;
const svgEl = container.querySelector(`[data-connection-svg="${conn.from}-${conn.to}"]`) as HTMLElement;
if (svgEl) {
const pathEl = svgEl.querySelector('path');
const arrowsGroup = svgEl.querySelector('.arrows-group');
if (pathEl) {
pathEl.setAttribute('d', createCurvedPath(fromX, fromY, toX, toY));
}
if (arrowsGroup) {
const arrows = getArrowPositions(fromX, fromY, toX, toY);
arrowsGroup.innerHTML = arrows.map(arrow =>
`<polygon points="0,-4 8,0 0,4" fill="${conn.color || '#9ca3af'}" transform="translate(${arrow.x},${arrow.y}) rotate(${arrow.angle})" opacity="0.8"/>`
).join('');
}
}
});
};
useEffect(() => {
updateConnections();
const handleResize = () => updateConnections();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [filteredNodes, filteredConnections]);
const handleNodeMouseOver = (nodeId: string) => {
const relatedNodes = new Set<string>();
const relatedConnections: string[] = [];
connections.forEach(conn => {
if (conn.from === nodeId || conn.to === nodeId) {
relatedNodes.add(conn.from);
relatedNodes.add(conn.to);
relatedConnections.push(`${conn.from}-${conn.to}`);
}
});
setHighlightedPath([...relatedNodes]);
};
const handleNodeMouseOut = () => {
setHighlightedPath([]);
};
const handleNodeDoubleClick = (node: SystemMapNode) => {
if (isDragging) return;
setSelectedNode(node);
setEditData({ title: node.title, description: node.description, group: node.group });
setIsEditing(false);
};
const handleMapClick = (e: React.MouseEvent) => {
if (e.target === mapContainerRef.current) {
setSelectedNode(null);
setIsEditing(false);
}
};
const handleNodeClick = (e: React.MouseEvent) => {
e.stopPropagation();
setSelectedNode(null);
setIsEditing(false);
};
const checkCollision = (nodeEl: HTMLElement, newX: number, newY: number, excludeId: string) => {
const nodeWidth = 240;
const nodeHeight = 140;
const margin = 20;
return filteredNodes.some(node => {
if (node.id === excludeId) return false;
const otherEl = mapContainerRef.current?.querySelector(`[data-node="${node.id}"]`) as HTMLElement;
if (!otherEl) return false;
const otherRect = otherEl.getBoundingClientRect();
const containerRect = mapContainerRef.current!.getBoundingClientRect();
const otherX = otherRect.left - containerRect.left;
const otherY = otherRect.top - containerRect.top;
return !(newX + nodeWidth + margin < otherX ||
newX > otherX + nodeWidth + margin ||
newY + nodeHeight + margin < otherY ||
newY > otherY + nodeHeight + margin);
});
};
const findNonCollidingPosition = (originalX: number, originalY: number, nodeId: string) => {
const step = 30;
const maxAttempts = 50;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const angle = (attempt * 45) % 360;
const distance = Math.floor(attempt / 8) * step;
const newX = originalX + Math.cos(angle * Math.PI / 180) * distance;
const newY = originalY + Math.sin(angle * Math.PI / 180) * distance;
if (newX < 0 || newY < 0 || newX > mapDimensions.width - 240 || newY > mapDimensions.height - 140) {
continue;
}
if (!checkCollision(document.createElement('div'), newX, newY, nodeId)) {
return { x: newX, y: newY };
}
}
return { x: originalX, y: originalY };
};
const handleNodeDragStart = (e: React.MouseEvent, nodeId: string) => {
e.preventDefault();
setIsDragging(true);
setDraggedNode(nodeId);
const startX = e.clientX;
const startY = e.clientY;
const nodeEl = mapContainerRef.current?.querySelector(`[data-node="${nodeId}"]`) as HTMLElement;
if (!nodeEl) return;
const initialRect = nodeEl.getBoundingClientRect();
const containerRect = mapContainerRef.current!.getBoundingClientRect();
const initialLeft = initialRect.left - containerRect.left;
const initialTop = initialRect.top - containerRect.top;
const handleMouseMove = (moveEvent: MouseEvent) => {
if (!mapContainerRef.current) return;
const deltaX = moveEvent.clientX - startX;
const deltaY = moveEvent.clientY - startY;
let newLeft = initialLeft + deltaX;
let newTop = initialTop + deltaY;
newLeft = Math.max(0, Math.min(newLeft, mapDimensions.width - 240));
newTop = Math.max(0, Math.min(newTop, mapDimensions.height - 140));
if (!checkCollision(nodeEl, newLeft, newTop, nodeId)) {
nodeEl.style.left = `${newLeft}px`;
nodeEl.style.top = `${newTop}px`;
} else {
const { x, y } = findNonCollidingPosition(newLeft, newTop, nodeId);
nodeEl.style.left = `${x}px`;
nodeEl.style.top = `${y}px`;
}
updateConnections();
setMapDimensions(getMapDimensions());
};
const handleMouseUp = () => {
setIsDragging(false);
setDraggedNode(null);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
setTimeout(() => setMapDimensions(getMapDimensions()), 100);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
const handleEditToggle = () => {
setIsEditing(!isEditing);
if (!isEditing && selectedNode) {
setEditData({ title: selectedNode.title, description: selectedNode.description, group: selectedNode.group });
}
};
const handleSaveEdit = () => {
if (selectedNode) {
console.log('Saving changes:', editData);
setIsEditing(false);
}
};
const resetLayout = () => {
if (!mapContainerRef.current) return;
filteredNodes.forEach(node => {
const nodeEl = mapContainerRef.current!.querySelector(`[data-node="${node.id}"]`) as HTMLElement;
if (nodeEl) {
nodeEl.style.left = `${node.left}px`;
nodeEl.style.top = `${node.top}px`;
}
});
updateConnections();
setMapDimensions(getMapDimensions());
};
return (
<div className="w-full h-screen bg-background overflow-hidden">
<div className="p-4 border-b bg-card">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground">
Mapa Interativo - TrackSteel
</h1>
<p className="text-sm text-muted-foreground">
Arraste os módulos, passe o mouse para ver fluxos e duplo-clique para detalhes
</p>
</div>
<div className="flex items-center gap-2">
<div className="flex gap-1">
{filters.map(filter => {
const Icon = filter.icon;
return (
<Button
key={filter.id}
variant={activeFilter === filter.id ? "default" : "outline"}
size="sm"
onClick={() => setActiveFilter(filter.id)}
className="gap-2"
>
<Icon className="w-4 h-4" />
{filter.label}
</Button>
);
})}
</div>
<Button
variant="outline"
size="sm"
onClick={resetLayout}
className="gap-2"
>
<RotateCcw className="w-4 h-4" />
Reset
</Button>
</div>
</div>
</div>
<div className="relative w-full overflow-auto" style={{ height: 'calc(100vh - 120px)' }}>
<div
ref={mapContainerRef}
className="relative bg-gradient-to-br from-background to-muted/20"
style={{
width: `${mapDimensions.width}px`,
height: `${mapDimensions.height}px`,
minWidth: '100%',
minHeight: '100%'
}}
onClick={handleMapClick}
>
<svg
className="absolute inset-0 w-full h-full pointer-events-none z-10"
style={{ overflow: 'visible' }}
>
{filteredConnections.map(conn => (
<g
key={`${conn.from}-${conn.to}`}
data-connection-svg={`${conn.from}-${conn.to}`}
>
<path
stroke={conn.color || '#9ca3af'}
strokeWidth={highlightedPath.includes(conn.from) && highlightedPath.includes(conn.to) ? "3" : "2"}
fill="none"
opacity={highlightedPath.includes(conn.from) && highlightedPath.includes(conn.to) ? "1" : "0.7"}
filter={highlightedPath.includes(conn.from) && highlightedPath.includes(conn.to) ? "drop-shadow(0 0 8px rgba(0,0,0,0.3))" : "none"}
/>
<g className="arrows-group" />
</g>
))}
</svg>
{filteredNodes.map(node => (
<div
key={node.id}
data-node={node.id}
className={cn(
"absolute p-4 bg-card border rounded-xl shadow-lg cursor-grab transition-all duration-300 z-20",
"hover:shadow-xl hover:scale-[1.02]",
draggedNode === node.id ? "cursor-grabbing scale-105 shadow-2xl z-30" : "",
highlightedPath.includes(node.id) ? "border-2 shadow-2xl" : "border",
!node.hasAccess ? "opacity-60 grayscale" : ""
)}
style={{
left: `${node.left}px`,
top: `${node.top}px`,
width: '240px',
minHeight: '140px',
borderColor: highlightedPath.includes(node.id) ? node.color : undefined,
boxShadow: highlightedPath.includes(node.id)
? `0 20px 40px ${node.color}30, 0 0 0 2px ${node.color}`
: undefined
}}
onMouseDown={(e) => handleNodeDragStart(e, node.id)}
onMouseOver={() => handleNodeMouseOver(node.id)}
onMouseOut={handleNodeMouseOut}
onDoubleClick={() => handleNodeDoubleClick(node)}
onClick={handleNodeClick}
>
<div className="flex items-start gap-2 mb-3">
<div
className="text-2xl flex-shrink-0"
style={{
filter: node.hasAccess ? 'none' : 'grayscale(1)',
textShadow: highlightedPath.includes(node.id) ? `0 0 10px ${node.color}80` : 'none'
}}
>
{node.icon}
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-sm text-foreground mb-1 leading-tight">
{node.title}
</h3>
<p className="text-xs text-muted-foreground mb-2 leading-relaxed">
{node.group}
</p>
</div>
</div>
<div className="flex flex-wrap gap-1">
{node.profiles.slice(0, 2).map((profile, index) => (
<Badge
key={index}
variant="secondary"
className="text-xs font-medium"
style={{
backgroundColor: `${node.color}20`,
color: node.color,
borderColor: `${node.color}40`,
border: '1px solid'
}}
>
{profile}
</Badge>
))}
{node.profiles.length > 2 && (
<Badge variant="outline" className="text-xs">
+{node.profiles.length - 2}
</Badge>
)}
</div>
</div>
))}
</div>
</div>
{selectedNode && (
<div className="fixed top-0 right-0 w-96 h-full bg-card border-l shadow-2xl z-50 transform transition-transform duration-300">
<div className="p-6 h-full overflow-y-auto">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<span className="text-2xl">{selectedNode.icon}</span>
{isEditing ? (
<Input
value={editData.title}
onChange={(e) => setEditData({ ...editData, title: e.target.value })}
className="text-xl font-bold"
/>
) : (
<h2 className="text-xl font-bold text-primary">
{selectedNode.title}
</h2>
)}
</div>
<div className="flex gap-2">
{isAdmin && (
<>
{isEditing ? (
<Button
variant="outline"
size="sm"
onClick={handleSaveEdit}
>
<Save className="w-4 h-4" />
</Button>
) : (
<Button
variant="outline"
size="sm"
onClick={handleEditToggle}
>
<Edit className="w-4 h-4" />
</Button>
)}
</>
)}
<Button
variant="ghost"
size="sm"
onClick={() => {
setSelectedNode(null);
setIsEditing(false);
}}
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
<div className="space-y-6">
<div>
<h4 className="font-semibold mb-2">Descrição</h4>
{isEditing ? (
<Textarea
value={editData.description}
onChange={(e) => setEditData({ ...editData, description: e.target.value })}
className="text-sm"
rows={3}
/>
) : (
<p className="text-sm text-muted-foreground">
{selectedNode.description}
</p>
)}
</div>
<div>
<h4 className="font-semibold mb-2">Informações</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">Grupo:</span>
{isEditing ? (
<Input
value={editData.group}
onChange={(e) => setEditData({ ...editData, group: e.target.value })}
className="w-32 h-6 text-xs"
/>
) : (
<Badge variant="outline">{selectedNode.group}</Badge>
)}
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Acesso:</span>
<Badge variant={selectedNode.hasAccess ? "default" : "destructive"}>
{selectedNode.hasAccess ? "Permitido" : "Bloqueado"}
</Badge>
</div>
</div>
</div>
<div>
<h4 className="font-semibold mb-2">Perfis de Acesso</h4>
<div className="flex flex-wrap gap-1">
{selectedNode.profiles.map(profile => (
<Badge key={profile} variant="secondary" className="text-xs">
{profile}
</Badge>
))}
</div>
</div>
{isAdmin && selectedNode && (
<UserResourcePermissions
resourceKey={selectedNode.id}
resourceName={selectedNode.title}
/>
)}
{selectedNode.url && (
<Button
className="w-full gap-2"
onClick={() => window.open(selectedNode.url, '_blank')}
>
Acessar Módulo
</Button>
)}
</div>
</div>
</div>
)}
</div>
);
}
+27
View File
@@ -0,0 +1,27 @@
import { useLocation } from "react-router-dom";
import { useEffect } from "react";
const NotFound = () => {
const location = useLocation();
useEffect(() => {
console.error(
"404 Error: User attempted to access non-existent route:",
location.pathname
);
}, [location.pathname]);
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center">
<h1 className="text-4xl font-bold mb-4">404</h1>
<p className="text-xl text-gray-600 mb-4">Oops! Page not found</p>
<a href="/" className="text-blue-500 hover:text-blue-700 underline">
Return to Home
</a>
</div>
</div>
);
};
export default NotFound;
+241
View File
@@ -0,0 +1,241 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { CheckCircle, Search, Download, Filter, Truck } from 'lucide-react';
import { useOFsConcluidas } from '@/hooks/useOFsConcluidas';
import { OFConcluidaCard } from '@/components/of/OFConcluidaCard';
import { toast } from 'sonner';
const OFsConcluidas = () => {
const [searchTerm, setSearchTerm] = useState('');
const [activeTab, setActiveTab] = useState('concluidas');
const {
ofsConcluidas,
ofsEntregues,
entregarOF,
reverterEntregueParaConcluida,
reverterConcluidaParaAtiva,
loading
} = useOFsConcluidas();
const handleEntregarOF = async (of: any) => {
try {
await entregarOF(of.id);
toast.success('OF marcada como entregue com sucesso!');
} catch (error) {
toast.error('Erro ao marcar OF como entregue');
}
};
const handleReverterEntregue = async (of: any) => {
try {
await reverterEntregueParaConcluida(of.id);
toast.success('OF revertida para concluída com sucesso!');
} catch (error) {
toast.error('Erro ao reverter OF entregue');
}
};
const handleReverterConcluida = async (of: any) => {
try {
await reverterConcluidaParaAtiva(of.id);
toast.success('OF reativada com sucesso!');
} catch (error) {
toast.error('Erro ao reativar OF');
}
};
const filterOFs = (ofs: any[]) => {
if (!searchTerm) return ofs;
return ofs.filter(of =>
of.num_of.toLowerCase().includes(searchTerm.toLowerCase()) ||
of.descritivo?.toLowerCase().includes(searchTerm.toLowerCase())
);
};
const filteredOfsConcluidas = filterOFs(ofsConcluidas);
const filteredOfsEntregues = filterOFs(ofsEntregues);
if (loading) {
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div className="text-center py-8">
<div className="animate-spin rounded-full h-6 w-6 sm:h-8 sm:w-8 border-b-2 border-primary mx-auto"></div>
<p className="text-slate-400 mt-2 text-sm">Carregando OFs...</p>
</div>
</div>
);
}
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground">OFs Concluídas</h1>
<p className="text-sm md:text-base text-muted-foreground">Histórico de ordens de fabricação finalizadas</p>
</div>
<div className="flex flex-col sm:flex-row gap-2 w-full sm:w-auto">
<Button
variant="outline"
className="mobile-full-width text-xs md:text-sm"
>
<Download className="w-3 h-3 md:w-4 md:h-4 mr-2" />
Exportar
</Button>
<Button
variant="outline"
className="mobile-full-width text-xs md:text-sm"
>
<Filter className="w-3 h-3 md:w-4 md:h-4 mr-2" />
Filtrar
</Button>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 md:grid-cols-3 bg-muted h-auto p-1">
<TabsTrigger
value="concluidas"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<CheckCircle className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Concluídas ({filteredOfsConcluidas.length})</span>
<span className="sm:hidden">Concl ({filteredOfsConcluidas.length})</span>
</TabsTrigger>
<TabsTrigger
value="entregues"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Truck className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Entregues ({filteredOfsEntregues.length})</span>
<span className="sm:hidden">Entr ({filteredOfsEntregues.length})</span>
</TabsTrigger>
<TabsTrigger
value="relatorios"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Download className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Relatórios</span>
<span className="sm:hidden">Rel</span>
</TabsTrigger>
</TabsList>
<TabsContent value="concluidas" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<CheckCircle className="h-4 w-4 md:h-5 md:w-5" />
OFs Concluídas
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile space-y-4">
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 sm:left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-3 h-3 sm:w-4 sm:h-4" />
<Input
placeholder="Buscar OFs concluídas..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 sm:pl-10 text-xs sm:text-sm"
/>
</div>
</div>
{filteredOfsConcluidas.length === 0 ? (
<div className="text-center py-6 md:py-8 text-muted-foreground">
<CheckCircle className="mx-auto h-8 w-8 md:h-12 md:w-12 mb-4 opacity-50" />
<p className="text-sm md:text-base">
{searchTerm ? 'Nenhuma OF concluída encontrada com esse termo' : 'Nenhuma OF concluída encontrada'}
</p>
<p className="text-xs md:text-sm mt-2">As OFs finalizadas aparecerão aqui</p>
</div>
) : (
<div className="grid gap-3 sm:gap-4 lg:gap-6">
{filteredOfsConcluidas.map((of) => (
<OFConcluidaCard
key={of.id}
of={of}
onEntregar={handleEntregarOF}
onReverterConcluida={handleReverterConcluida}
showEntregarButton={true}
showAdminButtons={true}
/>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="entregues" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Truck className="h-4 w-4 md:h-5 md:w-5" />
OFs Entregues
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile space-y-4">
<div className="flex flex-col sm:flex-row gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 sm:left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground w-3 h-3 sm:w-4 sm:h-4" />
<Input
placeholder="Buscar OFs entregues..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-8 sm:pl-10 text-xs sm:text-sm"
/>
</div>
</div>
{filteredOfsEntregues.length === 0 ? (
<div className="text-center py-6 md:py-8 text-muted-foreground">
<Truck className="mx-auto h-8 w-8 md:h-12 md:w-12 mb-4 opacity-50" />
<p className="text-sm md:text-base">
{searchTerm ? 'Nenhuma OF entregue encontrada com esse termo' : 'Nenhuma OF entregue encontrada'}
</p>
<p className="text-xs md:text-sm mt-2">As OFs entregues aparecerão aqui</p>
</div>
) : (
<div className="grid gap-3 sm:gap-4 lg:gap-6">
{filteredOfsEntregues.map((of) => (
<OFConcluidaCard
key={of.id}
of={of}
onReverterEntregue={handleReverterEntregue}
showEntregarButton={false}
showAdminButtons={true}
/>
))}
</div>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="relatorios" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Download className="h-4 w-4 md:h-5 md:w-5" />
Relatórios de OFs Concluídas
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<div className="text-center py-6 md:py-8 text-muted-foreground">
<Download className="mx-auto h-8 w-8 md:h-12 md:w-12 mb-4 opacity-50" />
<p className="text-sm md:text-base">Relatórios em desenvolvimento</p>
<p className="text-xs md:text-sm mt-2">Em breve você poderá gerar relatórios detalhados</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
};
export default OFsConcluidas;
+85
View File
@@ -0,0 +1,85 @@
import React, { useState } from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { HardHat, Building2, Users, Calendar, Settings, FileText } from 'lucide-react';
import { ObraDashboard } from '@/components/obra/ObraDashboard';
import { ObraSpecificDashboard } from '@/components/obra/ObraSpecificDashboard';
import { DiarioObraRDO } from '@/components/obra/DiarioObraRDO';
import { RelatoriosObra } from '@/components/obra/RelatoriosObra';
const Obra = () => {
const [obraAtual, setObraAtual] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<string>('dashboard');
const handleSelectObra = (ofNumber: string) => {
setObraAtual(ofNumber);
setActiveTab('dashboard');
};
const handleNavigateToRDO = () => {
setActiveTab('rdo');
};
const handleNavigateToRelatorios = () => {
setActiveTab('relatorios');
};
if (!obraAtual) {
return (
<div className="space-y-4 sm:space-y-6 p-2 sm:p-4 lg:p-6">
<div className="space-y-1 sm:space-y-2">
<h1 className="text-lg sm:text-2xl lg:text-3xl font-bold text-foreground">Obra</h1>
<p className="text-muted-foreground text-xs sm:text-sm">Sistema completo de gerenciamento de obra</p>
</div>
<ObraDashboard onSelectObra={handleSelectObra} />
</div>
);
}
return (
<div className="space-y-4 sm:space-y-6 p-2 sm:p-4 lg:p-6">
<div className="space-y-1 sm:space-y-2">
<h1 className="text-lg sm:text-2xl lg:text-3xl font-bold text-foreground">
Obra {obraAtual}
</h1>
<p className="text-muted-foreground text-xs sm:text-sm">Dashboard específico da obra selecionada</p>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-3 sm:space-y-4">
<TabsList className="grid w-full grid-cols-3 h-auto p-1">
<TabsTrigger value="dashboard" className="flex flex-col sm:flex-row items-center gap-1 sm:gap-2 text-xs sm:text-sm p-2 sm:p-3">
<Building2 className="w-3 h-3 sm:w-4 sm:h-4" />
<span>Obras</span>
</TabsTrigger>
<TabsTrigger value="rdo" className="flex flex-col sm:flex-row items-center gap-1 sm:gap-2 text-xs sm:text-sm p-2 sm:p-3">
<Calendar className="w-3 h-3 sm:w-4 sm:h-4" />
<span>RDO</span>
</TabsTrigger>
<TabsTrigger value="relatorios" className="flex flex-col sm:flex-row items-center gap-1 sm:gap-2 text-xs sm:text-sm p-2 sm:p-3">
<FileText className="w-3 h-3 sm:w-4 sm:h-4" />
<span>Relatório da Obra</span>
</TabsTrigger>
</TabsList>
<TabsContent value="dashboard" className="space-y-4">
<ObraSpecificDashboard
obraAtual={obraAtual}
onNavigateToRDO={handleNavigateToRDO}
onNavigateToRelatorios={handleNavigateToRelatorios}
/>
</TabsContent>
<TabsContent value="rdo" className="space-y-4">
<DiarioObraRDO obraAtual={obraAtual} />
</TabsContent>
<TabsContent value="relatorios" className="space-y-4">
<RelatoriosObra />
</TabsContent>
</Tabs>
</div>
);
};
export default Obra;
+57
View File
@@ -0,0 +1,57 @@
import React from 'react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Settings } from 'lucide-react';
import { CadastrosObra } from '@/components/obra/CadastrosObra';
import { useIsMobile } from '@/hooks/use-mobile';
import { usePermissionControl } from '@/hooks/usePermissionControl';
import { Card, CardContent } from '@/components/ui/card';
const ObraConfiguracoes = () => {
const isMobile = useIsMobile();
const { canAdmin } = usePermissionControl();
if (!canAdmin()) {
return (
<div className="space-y-4 sm:space-y-6 p-2 sm:p-4 lg:p-6">
<div className="space-y-1 sm:space-y-2">
<h1 className="text-lg sm:text-2xl lg:text-3xl font-bold text-foreground flex items-center gap-2">
<Settings className="w-5 h-5 sm:w-6 sm:h-6 lg:w-8 lg:h-8" />
Configurações da Obra
</h1>
<p className="text-muted-foreground text-xs sm:text-sm">
Gerencie os cadastros e configurações relacionadas às obras
</p>
</div>
<Card className="bg-card border-border">
<CardContent className="p-8 text-center">
<p className="text-muted-foreground">
Você não tem permissão para acessar as configurações da obra.
</p>
</CardContent>
</Card>
</div>
);
}
return (
<div className="space-y-4 sm:space-y-6 p-2 sm:p-4 lg:p-6">
<div className="space-y-1 sm:space-y-2">
<h1 className="text-lg sm:text-2xl lg:text-3xl font-bold text-foreground flex items-center gap-2">
<Settings className="w-5 h-5 sm:w-6 sm:h-6 lg:w-8 lg:h-8" />
Configurações da Obra
</h1>
<p className="text-muted-foreground text-xs sm:text-sm">
Gerencie os cadastros e configurações relacionadas às obras
</p>
</div>
<div className="space-y-4">
<CadastrosObra />
</div>
</div>
);
};
export default ObraConfiguracoes;
+132
View File
@@ -0,0 +1,132 @@
import { useState } from 'react';
import { useOFs } from '@/hooks/useOFs';
import { useNavigate } from 'react-router-dom';
import { NovaOFModal } from '@/components/of/NovaOFModal';
import { EditOFModal } from '@/components/of/EditOFModal';
import { toast } from 'sonner';
import { OrdensHeader } from '@/components/of/OrdensHeader';
import { OrdensFiltros } from '@/components/of/OrdensFiltros';
import { OrdensLista } from '@/components/of/OrdensLista';
import { usePermissionControl } from '@/hooks/usePermissionControl';
const OrdensFabricacao = () => {
const navigate = useNavigate();
const { ofs, loading, refetch, updateOF, deleteOF, concluirOF } = useOFs();
const { canCreate, canEdit, canDelete } = usePermissionControl();
const [searchTerm, setSearchTerm] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [showNovaOFModal, setShowNovaOFModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [selectedOF, setSelectedOF] = useState<any>(null);
const filteredOrders = ofs.filter(ordem => {
const matchesSearch = searchTerm === '' ||
ordem.num_of.toLowerCase().includes(searchTerm.toLowerCase()) ||
ordem.descritivo?.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === 'all' || ordem.status === statusFilter;
return matchesSearch && matchesStatus;
});
const handleNovaOFSuccess = () => {
refetch();
};
const handleEditOF = (ordem: any) => {
if (!canEdit()) return;
setSelectedOF(ordem);
setShowEditModal(true);
};
const handleDeleteOF = async (ordem: any) => {
if (!canDelete()) return;
try {
await deleteOF(ordem.id);
toast.success('Ordem de Fabricação excluída com sucesso!');
} catch (error) {
toast.error('Erro ao excluir Ordem de Fabricação');
}
};
const handleConcluirOF = async (ordem: any) => {
if (!canEdit()) return;
try {
await concluirOF(ordem.id);
toast.success('Ordem de Fabricação concluída com sucesso!');
} catch (error) {
toast.error('Erro ao concluir Ordem de Fabricação');
}
};
const handleVerCronograma = (ordem: any) => {
navigate('/ofs/cronograma');
};
const handleVerDashboard = (ordem: any) => {
navigate(`/dashboard-producao?of=${ordem.num_of}`);
};
if (loading) {
return (
<div className="space-y-4 sm:space-y-6 p-2 sm:p-4 lg:p-6 bg-background min-h-screen">
<div className="text-center py-8">
<div className="animate-spin rounded-full h-6 w-6 sm:h-8 sm:w-8 border-b-2 border-primary mx-auto"></div>
<p className="text-muted-foreground mt-2 text-sm">Carregando ordens...</p>
</div>
</div>
);
}
return (
<div className="space-y-3 sm:space-y-4 lg:space-y-6 p-2 sm:p-4 lg:p-6 bg-background min-h-screen">
<OrdensHeader
onNovaOF={canCreate() ? () => setShowNovaOFModal(true) : undefined}
onOFsConcluidas={() => navigate('/cadastro/ofs-concluidas')}
showNovaOF={canCreate()}
/>
<OrdensFiltros
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
statusFilter={statusFilter}
setStatusFilter={setStatusFilter}
/>
<OrdensLista
ordens={filteredOrders}
totalOrdens={ofs.length}
searchTerm={searchTerm}
statusFilter={statusFilter}
onVerCronograma={handleVerCronograma}
onVerDashboard={handleVerDashboard}
onEdit={canEdit() ? handleEditOF : undefined}
onConcluir={canEdit() ? handleConcluirOF : undefined}
onDelete={canDelete() ? handleDeleteOF : undefined}
canEdit={canEdit()}
canDelete={canDelete()}
/>
{canCreate() && (
<NovaOFModal
isOpen={showNovaOFModal}
onClose={() => setShowNovaOFModal(false)}
onSuccess={handleNovaOFSuccess}
/>
)}
{canEdit() && (
<EditOFModal
isOpen={showEditModal}
onClose={() => setShowEditModal(false)}
onSuccess={handleNovaOFSuccess}
ordem={selectedOF}
onUpdate={updateOF}
/>
)}
</div>
);
};
export default OrdensFabricacao;
+122
View File
@@ -0,0 +1,122 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button';
import { X, ChevronLeft, ChevronRight, Play, Pause } from 'lucide-react';
import { supabase } from '@/integrations/supabase/client';
import { OFCardOptimized } from '@/components/painel-industrial/OFCardOptimized';
import { PainelHeader } from '@/components/painel-industrial/PainelHeader';
interface OFData {
num_of: string;
descritivo?: string;
peso_total?: number;
data_prazo?: string;
}
const PainelIndustrial: React.FC = () => {
const navigate = useNavigate();
const [ofsAtivas, setOfsAtivas] = useState<OFData[]>([]);
const [currentPage, setCurrentPage] = useState(0);
const [autoRotate, setAutoRotate] = useState(true);
const [loading, setLoading] = useState(true);
const ofsPerPage = 4;
const totalPages = Math.ceil(ofsAtivas.length / ofsPerPage);
const currentOFs = ofsAtivas.slice(currentPage * ofsPerPage, (currentPage + 1) * ofsPerPage);
// Buscar OFs ativas
const fetchOfsAtivas = async () => {
try {
const { data, error } = await supabase
.from('ordens_fabricacao')
.select('num_of, descritivo, peso_total, data_prazo')
.eq('status', 'ativa')
.order('created_at', { ascending: false });
if (error) throw error;
setOfsAtivas(data || []);
} catch (error) {
console.error('Erro ao buscar OFs ativas:', error);
} finally {
setLoading(false);
}
};
// Auto-rotação das páginas
useEffect(() => {
if (!autoRotate || totalPages <= 1) return;
const interval = setInterval(() => {
setCurrentPage(prev => (prev + 1) % totalPages);
}, 15000);
return () => clearInterval(interval);
}, [autoRotate, totalPages]);
// Atualização automática dos dados
useEffect(() => {
fetchOfsAtivas();
const interval = setInterval(fetchOfsAtivas, 30000);
return () => clearInterval(interval);
}, []);
const handleClose = () => {
navigate('/dashboard');
};
const handlePrevPage = () => {
setCurrentPage(prev => prev > 0 ? prev - 1 : totalPages - 1);
};
const handleNextPage = () => {
setCurrentPage(prev => (prev + 1) % totalPages);
};
if (loading) {
return (
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="text-2xl font-semibold text-foreground">Carregando painel...</div>
</div>
);
}
return (
<div className="min-h-screen bg-background p-3 sm:p-6">
<PainelHeader
totalPages={totalPages}
currentPage={currentPage}
autoRotate={autoRotate}
onToggleAutoRotate={() => setAutoRotate(!autoRotate)}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
onClose={handleClose}
/>
{/* Grid de OFs - Layout Responsivo Melhorado */}
<div className="
grid
grid-cols-1
sm:grid-cols-1
md:grid-cols-2
xl:grid-cols-2
2xl:grid-cols-4
gap-4 sm:gap-6 lg:gap-8
mt-6 sm:mt-8
auto-rows-max
">
{currentOFs.map((of) => (
<OFCardOptimized key={of.num_of} ofData={of} />
))}
</div>
{ofsAtivas.length === 0 && (
<div className="text-center py-20">
<p className="text-xl sm:text-2xl text-muted-foreground">Nenhuma OF ativa encontrada</p>
</div>
)}
</div>
);
};
export default PainelIndustrial;
+258
View File
@@ -0,0 +1,258 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import { CalendarClock, Package, Save } from 'lucide-react';
import { usePecas, Peca } from '@/hooks/usePecas';
import { toast } from 'sonner';
import { supabase } from '@/integrations/supabase/client';
const PRIORIDADES = [
{ value: 'P1', label: 'P1 - Alta', color: 'bg-orange-900 text-white' },
{ value: 'P2', label: 'P2 - Média-Alta', color: 'bg-orange-700 text-white' },
{ value: 'P3', label: 'P3 - Média-Baixa', color: 'bg-orange-400 text-black' },
{ value: 'P4', label: 'P4 - Baixa', color: 'bg-orange-100 text-black' },
];
const getPrioridadeColor = (prioridade: string) => {
const prioridadeObj = PRIORIDADES.find(p => p.value === prioridade);
return prioridadeObj?.color || 'bg-orange-100 text-black';
};
export default function PlanejamentoProducao() {
const { pecas, loading, updatePeca } = usePecas();
const [selectedPecas, setSelectedPecas] = useState<Set<string>>(new Set());
const [prioridadeLote, setPrioridadeLote] = useState<string>('');
const [saving, setSaving] = useState(false);
const handleSelectPeca = (pecaId: string, checked: boolean) => {
const newSelected = new Set(selectedPecas);
if (checked) {
newSelected.add(pecaId);
} else {
newSelected.delete(pecaId);
}
setSelectedPecas(newSelected);
};
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedPecas(new Set(pecas.map(p => p.id)));
} else {
setSelectedPecas(new Set());
}
};
const handlePrioridadeChange = async (pecaId: string, prioridade: string) => {
try {
const peca = pecas.find(p => p.id === pecaId);
if (!peca) return;
const { error } = await supabase
.from('pecas')
.update({ prioridade })
.eq('id', pecaId);
if (error) throw error;
toast.success('Prioridade atualizada com sucesso!');
// A atualização será refletida através do hook usePecas
} catch (error) {
console.error('Erro ao atualizar prioridade:', error);
toast.error('Erro ao atualizar prioridade');
}
};
const handleDefinirPrioridadeLote = async () => {
if (selectedPecas.size === 0) {
toast.error('Selecione pelo menos uma peça');
return;
}
if (!prioridadeLote) {
toast.error('Selecione uma prioridade');
return;
}
setSaving(true);
try {
const { error } = await supabase
.from('pecas')
.update({ prioridade: prioridadeLote })
.in('id', Array.from(selectedPecas));
if (error) throw error;
toast.success(`Prioridade ${prioridadeLote} definida para ${selectedPecas.size} peças`);
setSelectedPecas(new Set());
setPrioridadeLote('');
} catch (error) {
console.error('Erro ao definir prioridade do lote:', error);
toast.error('Erro ao definir prioridade do lote');
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="min-h-screen bg-slate-900 p-6">
<div className="flex items-center justify-center h-64">
<div className="text-lg text-white">Carregando planejamento...</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-slate-900 p-6">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-white mb-2">Planejamento de Produção</h1>
<p className="text-slate-400">Defina prioridades para as peças na cadeia produtiva</p>
</div>
<Badge variant="secondary" className="bg-slate-700 text-slate-300 border-slate-600">
{pecas.length} {pecas.length === 1 ? 'peça' : 'peças'}
</Badge>
</div>
{/* Controles de Prioridade em Lote */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<CalendarClock className="h-5 w-5" />
Definir Prioridade em Lote
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<div className="flex-1">
<Select value={prioridadeLote} onValueChange={setPrioridadeLote}>
<SelectTrigger className="bg-slate-700 border-slate-600 text-white">
<SelectValue placeholder="Selecione uma prioridade" />
</SelectTrigger>
<SelectContent>
{PRIORIDADES.map(prioridade => (
<SelectItem key={prioridade.value} value={prioridade.value}>
<div className="flex items-center gap-2">
<div className={`w-4 h-4 rounded ${prioridade.color}`}></div>
{prioridade.label}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
onClick={handleDefinirPrioridadeLote}
disabled={saving || selectedPecas.size === 0 || !prioridadeLote}
className="bg-blue-600 hover:bg-blue-700"
>
<Save className="h-4 w-4 mr-2" />
{saving ? 'Salvando...' : `Definir para ${selectedPecas.size} peças`}
</Button>
</div>
<div className="text-sm text-slate-400">
Selecione as peças na tabela abaixo e escolha uma prioridade para aplicar em lote
</div>
</CardContent>
</Card>
{/* Tabela de Peças */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Package className="h-5 w-5" />
Peças Cadastradas
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow className="border-slate-700 hover:bg-slate-700/50">
<TableHead className="text-slate-300 w-12">
<Checkbox
checked={selectedPecas.size === pecas.length && pecas.length > 0}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead className="text-slate-300">OF</TableHead>
<TableHead className="text-slate-300">Fase</TableHead>
<TableHead className="text-slate-300">Marca</TableHead>
<TableHead className="text-slate-300">Descrição</TableHead>
<TableHead className="text-slate-300">Quantidade</TableHead>
<TableHead className="text-slate-300">Peso Total</TableHead>
<TableHead className="text-slate-300">Prioridade</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pecas.map((peca) => (
<TableRow key={peca.id} className="border-slate-700 hover:bg-slate-700/30">
<TableCell>
<Checkbox
checked={selectedPecas.has(peca.id)}
onCheckedChange={(checked) => handleSelectPeca(peca.id, checked === true)}
/>
</TableCell>
<TableCell className="text-slate-300 font-medium">{peca.of_number}</TableCell>
<TableCell className="text-slate-300">{peca.etapa_fase || '-'}</TableCell>
<TableCell className="text-slate-300 font-medium">{peca.marca}</TableCell>
<TableCell className="text-slate-300">{peca.descricao || '-'}</TableCell>
<TableCell className="text-slate-300 text-center">{peca.quantidade}</TableCell>
<TableCell className="text-slate-300 text-right">{Math.round(peca.peso_total || 0)} kg</TableCell>
<TableCell>
<Select
value={peca.prioridade || 'P4'}
onValueChange={(value) => handlePrioridadeChange(peca.id, value)}
>
<SelectTrigger className="w-32 bg-slate-700 border-slate-600">
<SelectValue />
</SelectTrigger>
<SelectContent>
{PRIORIDADES.map(prioridade => (
<SelectItem key={prioridade.value} value={prioridade.value}>
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded ${prioridade.color}`}></div>
{prioridade.value}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
{/* Legenda de Prioridades */}
<Card className="bg-slate-800/50 border-slate-700">
<CardHeader>
<CardTitle className="text-white text-lg">Legenda de Prioridades</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{PRIORIDADES.map(prioridade => (
<div key={prioridade.value} className="flex items-center gap-2">
<div className={`w-6 h-6 rounded ${prioridade.color}`}></div>
<span className="text-slate-300">{prioridade.label}</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</div>
);
}
+590
View File
@@ -0,0 +1,590 @@
import React, { useState, useMemo } from 'react';
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
import { KanbanPrioridadesFabricacao } from '@/components/prioridades/KanbanPrioridadesFabricacao';
import { PecaSelectorModal } from '@/components/prioridades/PecaSelectorModal';
import { PrioridadesPDF } from '@/components/prioridades/PrioridadesPDF';
import { FiltrosVisualizacao } from '@/components/prioridades/FiltrosVisualizacao';
import { useItensPrioridadeFabricacaoFiltrado } from '@/hooks/useItensPrioridadeFabricacaoFiltrado';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Plus, FileText, RefreshCw, Printer } from 'lucide-react';
import { toast } from 'sonner';
const PrioridadesFabricacao = () => {
const {
itensPorPrioridade,
loading,
atualizarQuantidade,
removerItem,
removerItensPorPrioridade,
transferirItem,
reorderItems,
refetch,
ofSelecionada,
faseSelecionada,
versaoAtual,
onFiltroChange
} = useItensPrioridadeFabricacaoFiltrado();
const [showPecaSelector, setShowPecaSelector] = useState(false);
const [showPrioridadesPDF, setShowPrioridadesPDF] = useState(false);
// Construir título dinâmico
const tituloCompleto = useMemo(() => {
if (ofSelecionada && faseSelecionada) {
return `Prioridades de Fabricação - OF: ${ofSelecionada} - FASE: ${faseSelecionada}`;
}
return 'Prioridades de Fabricação';
}, [ofSelecionada, faseSelecionada]);
const calcularTotalPecas = () => {
return Object.values(itensPorPrioridade).reduce((total, itens) => total + itens.length, 0);
};
const calcularPesoTotal = () => {
return Object.values(itensPorPrioridade).reduce((total, itens) => {
return total + itens.reduce((pesoItens, item) => pesoItens + (item.peso_total || 0), 0);
}, 0);
};
const handleAddPecas = async () => {
await refetch();
};
const generateTickBoxesHTML = (quantity: number): string => {
let boxesHtml = '';
if (quantity > 10) {
const numBigBoxes = Math.floor(quantity / 5);
const numSmallBoxes = quantity % 5;
for (let i = 0; i < numBigBoxes; i++) {
boxesHtml += `<div class="tick-box-large"><span>5</span></div>`;
}
for (let i = 0; i < numSmallBoxes; i++) {
boxesHtml += `<div class="tick-box"></div>`;
}
} else {
for (let i = 0; i < quantity; i++) {
boxesHtml += `<div class="tick-box"></div>`;
}
}
return `<div class="flex items-center flex-wrap gap-1">${boxesHtml}</div>`;
};
const handleImprimirRelatorio = async () => {
if (!ofSelecionada || !faseSelecionada) {
toast.error('Selecione uma OF e Fase para imprimir o relatório');
return;
}
try {
// Criar nova janela para impressão
const printWindow = window.open('', '_blank');
if (!printWindow) {
throw new Error('Não foi possível abrir janela de impressão');
}
// Obter data atual formatada
const dataAtual = new Date().toLocaleDateString('pt-BR');
// Gerar conteúdo dos itens por prioridade
let itemsContent = '';
['P1', 'P2', 'P3', 'P4'].forEach((codigo, priorityIndex) => {
const itens = itensPorPrioridade[codigo] || [];
if (itens.length === 0) return;
const getPrioridadeNome = (codigo: string) => {
switch (codigo) {
case 'P1': return 'Prioridade P1 - Urgente';
case 'P2': return 'Prioridade P2 - Alta';
case 'P3': return 'Prioridade P3 - Média';
case 'P4': return 'Prioridade P4 - Baixa';
default: return 'Desconhecida';
}
};
const getCoresPrioridade = (codigo: string) => {
switch (codigo) {
case 'P1': return 'text-red-700 bg-red-100';
case 'P2': return 'text-orange-700 bg-orange-100';
case 'P3': return 'text-blue-700 bg-blue-100';
case 'P4': return 'text-gray-700 bg-gray-200';
default: return 'text-gray-700 bg-gray-200';
}
};
// Adicionar classe page-break para P2, P3 e P4
const pageBreakClass = priorityIndex > 0 ? 'page-break' : '';
itemsContent += `
<div class="priority-group ${pageBreakClass}">
<h2 class="text-lg font-semibold ${getCoresPrioridade(codigo)} px-3 py-1 rounded-md inline-block mb-3">
${getPrioridadeNome(codigo)}
</h2>
<div class="space-y-1">
`;
// Gerar linhas de itens (3 por linha)
for (let i = 0; i < Math.ceil(itens.length / 3); i++) {
const bgColorClass = i % 2 !== 0 ? 'bg-gray-50' : 'bg-white';
const rowItems = itens.slice(i * 3, (i + 1) * 3);
itemsContent += `<div class="grid grid-cols-3 gap-2 p-1 rounded-md ${bgColorClass}">`;
rowItems.forEach((item) => {
const quantidade = item.quantidade_priorizada;
const marca = item.peca?.marca || 'N/A';
const temComponentes = item.peca?.tem_componentes;
const infoType = temComponentes ? '(C/M)' : '(S/M)';
const tickBoxes = generateTickBoxesHTML(quantidade);
itemsContent += `
<div class="item-card">
<div class="flex items-center flex-wrap gap-2 mb-2">
<span class="font-semibold text-sm whitespace-nowrap">${marca} (${quantidade})</span>
<span class="text-xs font-medium text-gray-500">${infoType}</span>
${tickBoxes}
</div>
<div class="mt-2 text-xs">
<div class="border-b border-gray-400 pb-1 h-5">Data/Operador:</div>
</div>
</div>
`;
});
// Preencher células vazias se necessário
for (let j = rowItems.length; j < 3; j++) {
itemsContent += `<div></div>`;
}
itemsContent += `</div>`;
}
itemsContent += `
</div>
</div>
`;
});
// HTML completo seguindo exatamente o modelo fornecido
const printContent = `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checklist de Produção por Prioridade</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
@media print {
body {
font-size: 9px;
}
.check-box-print {
border: 1px solid #333 !important;
}
.page-break {
page-break-before: always;
}
h2 {
page-break-after: avoid;
}
.item-card {
page-break-inside: avoid;
}
}
.item-card {
border: 1px solid #e5e7eb;
padding: 8px;
border-radius: 6px;
}
.tick-box {
width: 12px;
height: 12px;
border: 1px solid #6b7280;
display: inline-block;
flex-shrink: 0;
}
.tick-box-large {
width: 16px;
height: 16px;
border: 1px solid #6b7280;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
flex-shrink: 0;
}
.tick-box-large span {
color: #d1d5db;
font-size: 10px;
font-weight: 600;
}
</style>
</head>
<body class="bg-white">
<div class="max-w-4xl mx-auto p-6 sm:p-8">
<!-- Cabeçalho do Relatório -->
<div class="flex justify-between items-center border-b-2 border-gray-800 pb-4 mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900">Checklist de Produção</h1>
<p class="text-gray-600">Formulário para apontamento da fabricação.</p>
</div>
<div class="text-right">
<p class="font-semibold">Data de Emissão: <span class="font-normal">${dataAtual}</span>
${versaoAtual ? `<span class="ml-2 text-gray-500">Rev. ${versaoAtual.revisao}</span>` : ''}
</p>
</div>
</div>
<!-- Informações da OF e Fase (Layout Melhorado) -->
<div class="border border-gray-200 bg-white p-4 rounded-lg mb-2">
<div class="grid grid-cols-1 md:grid-cols-4 gap-x-6 gap-y-4">
<!-- Coluna OF -->
<div>
<p class="text-xs font-medium text-gray-500">Ordem de Fabricação (OF)</p>
<p class="text-base font-bold text-gray-800">${ofSelecionada}</p>
</div>
<!-- Coluna Fase -->
<div>
<p class="text-xs font-medium text-gray-500">Fase</p>
<p class="text-base font-bold text-gray-800">${faseSelecionada}</p>
</div>
<!-- Coluna Processo -->
<div class="md:col-span-2">
<p class="text-xs font-medium text-gray-500">PROCESSO</p>
<div class="flex items-center flex-wrap gap-x-4 gap-y-1 mt-1">
<div class="flex items-center gap-1"><div class="w-4 h-4 border-2 border-gray-500 check-box-print"></div><span class="text-sm font-semibold text-gray-700">Corte</span></div>
<div class="flex items-center gap-1"><div class="w-4 h-4 border-2 border-gray-500 check-box-print"></div><span class="text-sm font-semibold text-gray-700">Solda</span></div>
<div class="flex items-center gap-1"><div class="w-4 h-4 border-2 border-gray-500 check-box-print"></div><span class="text-sm font-semibold text-gray-700">Pintura</span></div>
<div class="flex items-center gap-1"><div class="w-4 h-4 border-2 border-gray-500 check-box-print"></div><span class="text-sm font-semibold text-gray-700">Expedição</span></div>
</div>
</div>
</div>
</div>
<!-- Legenda -->
<div class="text-xs text-gray-600 mb-6 flex items-center flex-wrap gap-x-3">
<span class="font-semibold">Legenda:</span>
<span>Marca (Qtd)</span>
<span class="font-medium text-gray-500">(S/M)</span>
<span>= Sem Montagem,</span>
<span class="font-medium text-gray-500">(C/M)</span>
<span>= Com Montagem. Os quadrados</span>
<div class="tick-box inline-block"></div>
<span>indicam o controle de peças fabricadas.</span>
</div>
<div id="main-container" class="space-y-8">
${itemsContent}
</div>
</div>
<script>
window.onload = function() {
setTimeout(() => {
window.print();
window.close();
}, 800);
}
</script>
</body>
</html>
`;
printWindow.document.write(printContent);
printWindow.document.close();
toast.success('Relatório enviado para impressão');
} catch (error) {
console.error('Erro ao imprimir:', error);
toast.error('Erro ao imprimir o relatório');
}
};
return (
<StandardPageLayout
title={tituloCompleto}
subtitle="Gerencie as prioridades de fabricação das peças"
>
<div className="space-y-6">
{/* Filtros de Visualização */}
<FiltrosVisualizacao
onFiltroChange={onFiltroChange}
ofSelecionada={ofSelecionada}
faseSelecionada={faseSelecionada}
/>
{/* Header com resumo e ações */}
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="flex flex-wrap gap-4">
<Card className="p-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Total de Peças:</span>
<Badge variant="secondary">{calcularTotalPecas()}</Badge>
</div>
</Card>
<Card className="p-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Peso Total:</span>
<Badge variant="secondary">{calcularPesoTotal().toFixed(1)} kg</Badge>
</div>
</Card>
{versaoAtual && (
<Card className="p-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">Revisão:</span>
<Badge variant="outline">{versaoAtual.revisao}</Badge>
</div>
</Card>
)}
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => refetch()}
disabled={loading}
>
<RefreshCw className="h-4 w-4 mr-2" />
Atualizar
</Button>
<Button
variant="outline"
size="sm"
onClick={handleImprimirRelatorio}
disabled={!ofSelecionada || !faseSelecionada}
>
<Printer className="h-4 w-4 mr-2" />
Imprimir
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setShowPrioridadesPDF(true)}
disabled={!ofSelecionada || !faseSelecionada}
>
<FileText className="h-4 w-4 mr-2" />
Gerar PDF
</Button>
<Button
size="sm"
onClick={() => setShowPecaSelector(true)}
>
<Plus className="h-4 w-4 mr-2" />
Adicionar Peças
</Button>
</div>
</div>
{/* Componente Kanban */}
{loading ? (
<div className="flex justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : ofSelecionada && faseSelecionada ? (
<KanbanPrioridadesFabricacao
itensPorPrioridade={itensPorPrioridade}
onUpdateQuantidade={atualizarQuantidade}
onRemoverItem={removerItem}
onRemoverItensPorPrioridade={removerItensPorPrioridade}
onTransferirItem={transferirItem}
onReorderItems={reorderItems}
/>
) : (
<Card className="p-8 text-center">
<CardContent>
<p className="text-muted-foreground">
Selecione uma OF e Fase nos filtros acima para visualizar o Kanban de prioridades.
</p>
</CardContent>
</Card>
)}
{/* Modais */}
<PecaSelectorModal
isOpen={showPecaSelector}
onClose={() => setShowPecaSelector(false)}
onAddPecas={handleAddPecas}
/>
<PrioridadesPDF
isOpen={showPrioridadesPDF}
onClose={() => setShowPrioridadesPDF(false)}
itensPorPrioridade={itensPorPrioridade}
versaoAtual={versaoAtual}
/>
{/* Componente oculto para impressão */}
<div className="hidden">
<div id="prioridades-pdf-content">
{(ofSelecionada && faseSelecionada) && (
<div className="bg-white text-black max-w-4xl mx-auto p-6">
{/* Usar o mesmo template do PDF */}
<div className="flex justify-between items-center border-b-2 border-gray-800 pb-4 mb-4">
<div>
<h1 className="text-2xl font-bold text-gray-900">Checklist de Produção</h1>
<p className="text-gray-600">Formulário para apontamento da fabricação.</p>
</div>
<div className="text-right">
<p className="font-semibold">
Data de Emissão: <span className="font-normal">{new Date().toLocaleDateString('pt-BR')}</span>
{versaoAtual && (
<span className="ml-2 text-gray-500">Rev. {versaoAtual.revisao}</span>
)}
</p>
</div>
</div>
<div className="border border-gray-200 bg-white p-4 rounded-lg mb-2">
<div className="grid grid-cols-1 md:grid-cols-4 gap-x-6 gap-y-4">
<div>
<p className="text-xs font-medium text-gray-500">Ordem de Fabricação (OF)</p>
<p className="text-base font-bold text-gray-800">{ofSelecionada}</p>
</div>
<div>
<p className="text-xs font-medium text-gray-500">Fase</p>
<p className="text-base font-bold text-gray-800">{faseSelecionada}</p>
</div>
<div className="md:col-span-2">
<p className="text-xs font-medium text-gray-500">PROCESSO</p>
<div className="flex items-center flex-wrap gap-x-4 gap-y-1 mt-1">
{['Corte', 'Solda', 'Pintura', 'Expedição'].map((processo) => (
<div key={processo} className="flex items-center gap-1">
<div className="w-4 h-4 border-2 border-gray-500"></div>
<span className="text-sm font-semibold text-gray-700">{processo}</span>
</div>
))}
</div>
</div>
</div>
</div>
<div className="text-xs text-gray-600 mb-6 flex items-center flex-wrap gap-x-3">
<span className="font-semibold">Legenda:</span>
<span>Marca (Qtd)</span>
<span className="font-medium text-gray-500">(S/M)</span>
<span>= Sem Montagem,</span>
<span className="font-medium text-gray-500">(C/M)</span>
<span>= Com Montagem. Os quadrados indicam o controle de peças fabricadas.</span>
</div>
{/* Renderizar os itens por prioridade */}
<div className="space-y-8">
{['P1', 'P2', 'P3', 'P4'].map((codigo, priorityIndex) => {
const itens = itensPorPrioridade[codigo] || [];
if (itens.length === 0) return null;
const getPrioridadeNome = (codigo: string) => {
switch (codigo) {
case 'P1': return 'Prioridade P1 - Urgente';
case 'P2': return 'Prioridade P2 - Alta';
case 'P3': return 'Prioridade P3 - Média';
case 'P4': return 'Prioridade P4 - Baixa';
default: return 'Desconhecida';
}
};
const getCoresPrioridade = (codigo: string) => {
switch (codigo) {
case 'P1': return 'text-red-700 bg-red-100';
case 'P2': return 'text-orange-700 bg-orange-100';
case 'P3': return 'text-blue-700 bg-blue-100';
case 'P4': return 'text-gray-700 bg-gray-200';
default: return 'text-gray-700 bg-gray-200';
}
};
return (
<div key={codigo}>
<h2 className={`text-lg font-semibold ${getCoresPrioridade(codigo)} px-3 py-1 rounded-md inline-block mb-3`}>
{getPrioridadeNome(codigo)}
</h2>
<div className="space-y-1">
{Array.from({ length: Math.ceil(itens.length / 3) }, (_, i) => {
const bgColorClass = i % 2 !== 0 ? 'bg-gray-50' : 'bg-white';
const rowItems = itens.slice(i * 3, (i + 1) * 3);
return (
<div key={i} className={`grid grid-cols-3 gap-2 p-1 rounded-md ${bgColorClass}`}>
{rowItems.map((item) => {
const quantidade = item.quantidade_priorizada;
const marca = item.peca?.marca || 'N/A';
const temComponentes = item.peca?.tem_componentes;
const infoType = temComponentes ? '(C/M)' : '(S/M)';
// Gerar checkboxes
const generateTickBoxes = (quantity: number) => {
const boxes = [];
if (quantity > 10) {
const numBigBoxes = Math.floor(quantity / 5);
const numSmallBoxes = quantity % 5;
for (let i = 0; i < numBigBoxes; i++) {
boxes.push(
<div key={`big-${i}`} className="tick-box-large">
<span>5</span>
</div>
);
}
for (let i = 0; i < numSmallBoxes; i++) {
boxes.push(<div key={`small-${i}`} className="tick-box"></div>);
}
} else {
for (let i = 0; i < quantity; i++) {
boxes.push(<div key={i} className="tick-box"></div>);
}
}
return <div className="flex items-center flex-wrap gap-1">{boxes}</div>;
};
return (
<div key={item.id} className="item-card">
<div className="flex items-center flex-wrap gap-2 mb-2">
<span className="font-semibold text-sm whitespace-nowrap">
{marca} ({quantidade})
</span>
<span className="text-xs font-medium text-gray-500">{infoType}</span>
{generateTickBoxes(quantidade)}
</div>
<div className="mt-2 text-xs">
<div className="border-b border-gray-400 pb-1 h-5">Data/Operador:</div>
</div>
</div>
);
})}
{/* Preencher células vazias se necessário */}
{Array.from({ length: 3 - rowItems.length }, (_, emptyIndex) => (
<div key={`empty-${emptyIndex}`}></div>
))}
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
</StandardPageLayout>
);
};
export default PrioridadesFabricacao;
@@ -0,0 +1,39 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CheckCircle } from 'lucide-react';
const PrioridadesFabricacaoSimples = () => {
console.log('🎯 PrioridadesFabricacaoSimples carregando...');
return (
<div className="space-y-6 p-6">
<div className="flex items-center gap-3">
<CheckCircle className="h-8 w-8 text-green-500" />
<div>
<h1 className="text-3xl font-bold text-foreground">Prioridades de Fabricação</h1>
<p className="text-muted-foreground">
Componente carregado com sucesso!
</p>
</div>
</div>
<Card className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
<CardHeader>
<CardTitle className="text-green-800 dark:text-green-200">
Status do Componente
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2 text-green-700 dark:text-green-300">
<p> Rota funcionando: /producao/prioridades</p>
<p> Componente renderizando sem erros</p>
<p> Layout aplicado corretamente</p>
<p> Usuário autenticado com sucesso</p>
</div>
</CardContent>
</Card>
</div>
);
};
export default PrioridadesFabricacaoSimples;
+41
View File
@@ -0,0 +1,41 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { FileText } from 'lucide-react';
import { ApontamentoDiarioChart } from '@/components/producao/ApontamentoDiarioChart';
import { RelatorioPecasProcessoModal } from '@/components/producao/RelatorioPecasProcessoModal';
import { useState } from 'react';
const Producao = () => {
const [showRelatorioPecasProcesso, setShowRelatorioPecasProcesso] = useState(false);
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground mb-2">Produção</h1>
<p className="text-sm md:text-base text-muted-foreground">Controle e monitoramento da produção</p>
</div>
<Button
onClick={() => setShowRelatorioPecasProcesso(true)}
className="bg-red-600 hover:bg-red-700 text-white flex items-center gap-2 px-4 py-2"
>
<FileText className="h-4 w-4" />
Relatório Pçs p/ Processo
</Button>
</div>
{/* Gráfico de Apontamento Diário */}
<ApontamentoDiarioChart />
{/* Modal do Relatório de Peças por Processo */}
<RelatorioPecasProcessoModal
isOpen={showRelatorioPecasProcesso}
onClose={() => setShowRelatorioPecasProcesso(false)}
/>
</div>
);
};
export default Producao;
+81
View File
@@ -0,0 +1,81 @@
import React from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ArrowLeft, Package } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { useOFs } from '@/hooks/useOFs';
export default function SeletorOF() {
const navigate = useNavigate();
const { ofs, loading } = useOFs();
const handleOFSelect = (ofNumber: string) => {
navigate(`/cadastro-pecas/${encodeURIComponent(ofNumber)}`);
};
const handleVoltar = () => {
navigate(-1);
};
if (loading) {
return (
<div className="min-h-screen bg-background p-6">
<div className="flex items-center justify-center h-64">
<div className="text-lg text-foreground">Carregando OFs ativas...</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-background p-6">
<div className="max-w-4xl mx-auto space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">Selecionar OF</h1>
<p className="text-muted-foreground">Escolha uma OF para acessar o cadastro de peças</p>
</div>
<Button
onClick={handleVoltar}
variant="outline"
className="flex items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Voltar
</Button>
</div>
{/* OFs Grid */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-card-foreground flex items-center gap-2">
<Package className="h-5 w-5" />
OFs Ativas Disponíveis
</CardTitle>
</CardHeader>
<CardContent>
{ofs.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
Nenhuma OF ativa encontrada
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{ofs.map((of) => (
<Button
key={of.id}
onClick={() => handleOFSelect(of.num_of)}
variant="outline"
className="h-16 text-lg font-semibold hover:bg-primary hover:text-primary-foreground transition-colors"
>
{of.num_of}
</Button>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
+157
View File
@@ -0,0 +1,157 @@
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Database, Package, Info, Activity } from 'lucide-react';
import { useDatabaseUsage } from '@/hooks/useDatabaseUsage';
const Sistema = () => {
const {
databaseInfo,
loading,
error
} = useDatabaseUsage();
// Versão extraída do package.json
const version = "1.0.0";
// Simulação de dados de ocupação (em produção seria calculado com base no plano)
const calculateUsagePercentage = (sizeString: string) => {
if (!sizeString) return 0;
// Extrai o número da string (ex: "2048 kB" -> 2048)
const match = sizeString.match(/(\d+(?:\.\d+)?)\s*(\w+)/);
if (!match) return 0;
const size = parseFloat(match[1]);
const unit = match[2].toLowerCase();
// Converte para MB
let sizeInMB = size;
if (unit === 'kb') sizeInMB = size / 1024;
else if (unit === 'gb') sizeInMB = size * 1024;
else if (unit === 'tb') sizeInMB = size * 1024 * 1024;
// Assume limite de 500MB para o plano gratuito
const limitMB = 500;
return Math.min(sizeInMB / limitMB * 100, 100);
};
const usagePercentage = databaseInfo ? calculateUsagePercentage(databaseInfo.database_size) : 0;
return (
<div className="container mx-auto p-4 space-y-6 bg-background min-h-screen">
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold text-foreground">Painel do Sistema</h1>
<Badge variant="outline" className="text-sm bg-card text-card-foreground border-border">
Versão {version}
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{/* Informações da Versão */}
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-card-foreground">
Versão do Sistema
</CardTitle>
<Info className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-card-foreground">{version}</div>
<p className="text-xs text-muted-foreground mt-1">
Última atualização: {new Date().toLocaleDateString('pt-BR')}
</p>
</CardContent>
</Card>
{/* Uso do Banco de Dados */}
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-card-foreground">
Uso do Banco de Dados
</CardTitle>
<Database className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
{loading ? (
<div className="text-sm text-muted-foreground">Carregando...</div>
) : error ? (
<div className="text-sm text-destructive">Erro ao carregar dados</div>
) : (
<div className="space-y-3">
<div className="text-2xl font-bold text-card-foreground">
{databaseInfo?.database_size || 'N/A'}
</div>
<Progress value={usagePercentage} className="h-2" />
<p className="text-xs text-muted-foreground">
{usagePercentage.toFixed(1)}% utilizado
</p>
</div>
)}
</CardContent>
</Card>
{/* Número de Tabelas */}
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-card-foreground">
Tabelas do Sistema
</CardTitle>
<Package className="h-4 w-4 text-primary" />
</CardHeader>
<CardContent>
{loading ? (
<div className="text-sm text-muted-foreground">Carregando...</div>
) : error ? (
<div className="text-sm text-destructive">Erro ao carregar dados</div>
) : (
<div>
<div className="text-2xl font-bold text-card-foreground">
{databaseInfo?.table_count || 0}
</div>
<p className="text-xs text-muted-foreground mt-1">
Tabelas ativas
</p>
</div>
)}
</CardContent>
</Card>
</div>
{/* Status do Sistema */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-card-foreground flex items-center gap-2">
<Activity className="h-5 w-5 text-primary" />
Status do Sistema
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="flex items-center justify-between p-3 rounded-lg bg-muted border border-border">
<span className="text-card-foreground">Banco de Dados</span>
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-400">
Ativo
</Badge>
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted border border-border">
<span className="text-card-foreground">Autenticação</span>
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-400">
Ativo
</Badge>
</div>
<div className="flex items-center justify-between p-3 rounded-lg bg-muted border border-border">
<span className="text-card-foreground">Storage</span>
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 dark:bg-green-900/20 dark:text-green-400 dark:border-green-400">
Ativo
</Badge>
</div>
</div>
</CardContent>
</Card>
</div>
);
};
export default Sistema;
+469
View File
@@ -0,0 +1,469 @@
import React, { useState } from 'react';
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
import { StandardCard } from '@/components/layout/StandardCard';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Plus, Printer, Edit, Trash2, Eye, RefreshCw, Check, ShoppingCart } from 'lucide-react';
import { useSolicitacoesCompra } from '@/hooks/useSolicitacoesCompra';
import { useUserFunction } from '@/hooks/useUserFunction';
import { SolicitacaoComprasModal } from '@/components/solicitacao-compras/SolicitacaoComprasModal';
import { SolicitacaoComprasPreviewModal } from '@/components/solicitacao-compras/SolicitacaoComprasPreviewModal';
import { UserAvatar } from '@/components/ui/user-avatar';
import { format } from 'date-fns';
import { ptBR } from 'date-fns/locale';
const statusColors = {
'Em planejamento': 'bg-yellow-500',
'Revisado': 'bg-orange-500',
'Solicitado': 'bg-blue-500',
'Comprado': 'bg-blue-600',
'Recebido': 'bg-lime-500',
'Arquivado': 'bg-red-500',
};
export default function SolicitacaoCompras() {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [selectedSolicitacao, setSelectedSolicitacao] = useState(null);
const { isComprador } = useUserFunction();
const {
solicitacoes,
isLoading,
canEdit,
canDelete,
deleteSolicitacao,
updateStatus,
revisar,
aceitar,
comprar,
comprarDireto,
isRevisando,
isAceitando,
isComprando,
isComprandoDireto
} = useSolicitacoesCompra();
const handlePrint = (solicitacao: any) => {
try {
const printContent = `
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Solicitação de Compra - ${solicitacao.numero_sc}</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Inter', sans-serif;
}
@media print {
body {
-webkit-print-color-adjust: exact;
color-adjust: exact;
}
.print-container {
box-shadow: none;
margin: 0;
max-width: 100%;
border: 1px solid #ddd;
}
}
</style>
</head>
<body class="bg-gray-100 p-4 sm:p-6">
<div class="print-container max-w-4xl mx-auto bg-white rounded-xl shadow-lg overflow-hidden">
<header class="bg-gray-100 text-gray-800 p-4 md:p-5 border-b border-gray-200">
<h1 class="text-xl md:text-2xl font-bold">Solicitação de Compra</h1>
<p class="text-gray-600 text-sm">Revise os detalhes da sua solicitação abaixo.</p>
</header>
<main class="p-4 md:p-6">
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
<div class="bg-gray-50 p-3 rounded-lg">
<h3 class="font-semibold text-gray-500 text-xs">Número SC</h3>
<p class="text-gray-900 text-base font-medium">${solicitacao.numero_sc}</p>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<h3 class="font-semibold text-gray-500 text-xs">Data da Solicitação</h3>
<p class="text-gray-900 text-base font-medium">${format(new Date(solicitacao.data_solicitacao), 'dd/MM/yyyy', { locale: ptBR })}</p>
</div>
<div class="bg-blue-50 border-l-4 border-blue-500 p-3 rounded-lg col-span-1 sm:col-span-2 lg:col-span-1">
<h3 class="font-semibold text-blue-800 text-xs">Status</h3>
<p class="text-blue-900 text-base font-medium">${solicitacao.status}</p>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<h3 class="font-semibold text-gray-500 text-xs">Solicitante</h3>
<p class="text-gray-900 text-base font-medium">${solicitacao.creator?.full_name || 'Usuário não encontrado'}</p>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<h3 class="font-semibold text-gray-500 text-xs">Objetivo</h3>
<p class="text-gray-900 text-base font-medium">${solicitacao.objetivo || 'N/A'}</p>
</div>
<div class="bg-gray-50 p-3 rounded-lg">
<h3 class="font-semibold text-gray-500 text-xs">OF / Revisão</h3>
<p class="text-gray-900 text-base font-medium">${solicitacao.of_number || 'N/A'} / Rev. ${solicitacao.revisao}</p>
</div>
<div class="bg-gray-50 p-3 rounded-lg col-span-1 sm:col-span-2 lg:col-span-3">
<h3 class="font-semibold text-gray-500 text-xs">Justificativa</h3>
<p class="text-gray-900 text-base font-medium">${solicitacao.justificativa || 'N/A'}</p>
</div>
</div>
${solicitacao.itens && solicitacao.itens.length > 0 ? `
<div class="mb-6">
<h2 class="text-lg font-bold text-gray-800 mb-3">Itens Solicitados</h2>
<div class="overflow-x-auto rounded-lg border border-gray-200">
<table class="min-w-full divide-y divide-gray-200 text-xs">
<thead class="bg-gray-50">
<tr>
<th scope="col" class="px-4 py-2 text-left text-xs font-bold text-gray-600 uppercase tracking-wider">Descrição</th>
<th scope="col" class="px-4 py-2 text-center text-xs font-bold text-gray-600 uppercase tracking-wider">Unidade</th>
<th scope="col" class="px-4 py-2 text-center text-xs font-bold text-gray-600 uppercase tracking-wider">Quantidade</th>
<th scope="col" class="px-4 py-2 text-right text-xs font-bold text-gray-600 uppercase tracking-wider">Prazo Recebimento</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
${solicitacao.itens.map((item: any, index: number) => `
<tr${index % 2 === 1 ? ' class="hover:bg-gray-50"' : ''}>
<td class="px-4 py-3 text-xs font-medium text-gray-800 break-words">${item.material?.descricao || 'Material não encontrado'}</td>
<td class="px-4 py-3 whitespace-nowrap text-center text-gray-600 text-xs">${item.material?.unidade || 'UN'}</td>
<td class="px-4 py-3 whitespace-nowrap text-center text-gray-600 text-xs">${item.quantidade}</td>
<td class="px-4 py-3 whitespace-nowrap text-right text-gray-600 text-xs">${format(new Date(item.prazo_recebimento), 'dd/MM/yyyy', { locale: ptBR })}</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
</div>
` : ''}
</main>
<footer class="text-center text-xs text-gray-400 p-3 bg-gray-50 border-t">
<p>Gerado em: ${new Date().toLocaleString('pt-BR')}</p>
</footer>
</div>
</body>
</html>
`;
const printWindow = window.open('', '_blank');
if (printWindow) {
printWindow.document.write(printContent);
printWindow.document.close();
// Aguardar o carregamento antes de focar e imprimir
printWindow.onload = function() {
printWindow.focus();
setTimeout(() => {
printWindow.print();
}, 250);
};
// Fallback caso onload não funcione
setTimeout(() => {
if (printWindow && !printWindow.closed) {
printWindow.focus();
printWindow.print();
}
}, 1000);
}
} catch (error) {
console.error('Erro ao imprimir:', error);
alert('Erro ao imprimir. Tente novamente.');
}
};
const handleEdit = (solicitacao: any) => {
if (!canEdit(solicitacao)) {
alert('Você só pode editar suas próprias solicitações.');
return;
}
setSelectedSolicitacao(solicitacao);
setIsModalOpen(true);
};
const handlePreview = (solicitacao: any) => {
setSelectedSolicitacao(solicitacao);
setIsPreviewOpen(true);
};
const handleDelete = (solicitacao: any) => {
if (!canDelete(solicitacao)) {
if (solicitacao.status !== 'Em planejamento') {
alert('⚠️ PERMISSÃO NEGADA\n\nSolicitações com status diferente de "Em planejamento" só podem ser excluídas por compradores.');
} else {
alert('⚠️ PERMISSÃO NEGADA\n\nVocê só pode excluir suas próprias solicitações.');
}
return;
}
if (window.confirm('⚠️ CONFIRMAÇÃO DE EXCLUSÃO\n\nTem certeza que deseja excluir esta solicitação de compra?\n\nEsta ação não pode ser desfeita!')) {
deleteSolicitacao(solicitacao.id);
}
};
const handleRevisar = (solicitacao: any) => {
if (window.confirm(`Tem certeza que deseja enviar a solicitação ${solicitacao.numero_sc} para revisão?`)) {
revisar({ id: solicitacao.id, created_by: solicitacao.created_by });
}
};
const handleAceitar = (solicitacao: any) => {
if (window.confirm(`Tem certeza que deseja aceitar a solicitação ${solicitacao.numero_sc}?`)) {
aceitar({ id: solicitacao.id, created_by: solicitacao.created_by });
}
};
const handleComprar = (solicitacao: any) => {
if (window.confirm(`Tem certeza que deseja marcar a solicitação ${solicitacao.numero_sc} como comprada?`)) {
comprar({ id: solicitacao.id, created_by: solicitacao.created_by });
}
};
const handleComprarDireto = (solicitacao: any) => {
if (window.confirm(`Você deseja passar o status dessa SC ${solicitacao.numero_sc} para "Comprado"?`)) {
comprarDireto({ id: solicitacao.id, created_by: solicitacao.created_by });
}
};
return (
<StandardPageLayout
title="Solicitação de Compras"
subtitle="Gerencie suas solicitações de compras de materiais"
>
<StandardCard title="Solicitações de Compra">
<div className="flex justify-between items-center mb-6">
<Button
onClick={() => setIsModalOpen(true)}
className="flex items-center gap-2"
>
<Plus className="h-4 w-4" />
Nova Solicitação
</Button>
</div>
{isLoading ? (
<div className="text-center py-8">Carregando solicitações...</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-border">
<thead className="bg-muted">
<tr>
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
Número SC
</th>
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
Usuário
</th>
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
Data
</th>
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-24">
OF/Objetivo
</th>
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
Status
</th>
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-16">
Revisão
</th>
<th className="px-2 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-16">
Itens
</th>
<th className="px-2 py-3 text-right text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
Ações
</th>
</tr>
</thead>
<tbody className="bg-background divide-y divide-border">
{solicitacoes.map((solicitacao) => (
<tr key={solicitacao.id} className="hover:bg-muted/50">
<td className="px-2 py-3 whitespace-nowrap">
<div className="text-sm font-medium text-foreground">
{solicitacao.numero_sc}
</div>
</td>
<td className="px-2 py-3 whitespace-nowrap">
<div className="flex items-center gap-2">
<UserAvatar
imageUrl={solicitacao.creator?.profile_image_url}
name={solicitacao.creator?.full_name}
email={solicitacao.creator?.email}
size="sm"
/>
<div className="text-sm text-foreground">
{solicitacao.creator?.full_name || 'Usuário não encontrado'}
</div>
</div>
</td>
<td className="px-2 py-3 whitespace-nowrap">
<div className="text-sm text-muted-foreground">
{format(new Date(solicitacao.data_solicitacao), 'dd/MM/yyyy', { locale: ptBR })}
</div>
</td>
<td className="px-2 py-3 whitespace-nowrap">
<div className="text-sm text-foreground">
{solicitacao.of_number || solicitacao.objetivo}
</div>
</td>
<td className="px-2 py-3 whitespace-nowrap">
<Badge
variant="secondary"
className={`${statusColors[solicitacao.status as keyof typeof statusColors]} text-white text-xs ${
isComprador && solicitacao.status === 'Em planejamento' ? 'cursor-pointer hover:opacity-80 select-none' : ''
}`}
onDoubleClick={
isComprador && solicitacao.status === 'Em planejamento'
? (e) => {
e.preventDefault();
e.stopPropagation();
handleComprarDireto(solicitacao);
}
: undefined
}
title={
isComprador && solicitacao.status === 'Em planejamento'
? 'Duplo clique para marcar como "Comprado"'
: undefined
}
>
{solicitacao.status}
</Badge>
</td>
<td className="px-2 py-3 whitespace-nowrap">
<div className="text-sm text-muted-foreground">
{solicitacao.revisao}
</div>
</td>
<td className="px-2 py-3 whitespace-nowrap">
<div className="text-sm text-muted-foreground">
{solicitacao.itens?.length || 0}
</div>
</td>
<td className="px-2 py-3 whitespace-nowrap text-right text-sm font-medium">
<div className="flex justify-end space-x-1">
{/* Ações básicas - sempre visíveis */}
<Button
variant="ghost"
size="sm"
onClick={() => handlePreview(solicitacao)}
title="Visualizar"
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handlePrint(solicitacao)}
title="Imprimir"
>
<Printer className="h-4 w-4" />
</Button>
{/* Ações condicionais baseadas em permissões */}
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(solicitacao)}
title="Editar"
disabled={!canEdit(solicitacao)}
className={!canEdit(solicitacao) ? "opacity-50 cursor-not-allowed" : ""}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(solicitacao)}
title="Excluir"
disabled={!canDelete(solicitacao)}
className={`${!canDelete(solicitacao) ? "opacity-50 cursor-not-allowed" : "text-red-600 hover:text-red-700"}`}
>
<Trash2 className="h-4 w-4" />
</Button>
{/* Ações exclusivas para compradores */}
{isComprador && (
<>
{/* Botão Revisar - só para status 'Em planejamento' */}
{solicitacao.status === 'Em planejamento' && (
<Button
variant="ghost"
size="sm"
onClick={() => handleRevisar(solicitacao)}
title="Enviar para Revisão"
disabled={isRevisando}
className="text-orange-600 hover:text-orange-700"
>
<RefreshCw className="h-4 w-4" />
</Button>
)}
{/* Botão Aceitar - para status 'Em planejamento' e 'Revisado' */}
{(solicitacao.status === 'Em planejamento' || solicitacao.status === 'Revisado') && (
<Button
variant="ghost"
size="sm"
onClick={() => handleAceitar(solicitacao)}
title="Aceitar Solicitação"
disabled={isAceitando}
className="text-green-600 hover:text-green-700"
>
<Check className="h-4 w-4" />
</Button>
)}
{/* Botão Comprar - só para status 'Solicitado' */}
{solicitacao.status === 'Solicitado' && (
<Button
variant="ghost"
size="sm"
onClick={() => handleComprar(solicitacao)}
title="Marcar como Comprado"
disabled={isComprando}
className="text-blue-600 hover:text-blue-700"
>
<ShoppingCart className="h-4 w-4" />
</Button>
)}
</>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</StandardCard>
<SolicitacaoComprasModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedSolicitacao(null);
}}
solicitacao={selectedSolicitacao}
/>
<SolicitacaoComprasPreviewModal
isOpen={isPreviewOpen}
onClose={() => {
setIsPreviewOpen(false);
setSelectedSolicitacao(null);
}}
solicitacao={selectedSolicitacao}
/>
</StandardPageLayout>
);
}
+234
View File
@@ -0,0 +1,234 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { MessageSquare, Send, Clock, CheckCircle, XCircle, User, Calendar, History } from 'lucide-react';
import { useSugestoes } from '@/hooks/useSugestoes';
import { useUserRole } from '@/hooks/useUserRole';
import { format, parseISO } from 'date-fns';
import { ptBR } from 'date-fns/locale';
import HistoricoSugestoes from '@/components/sugestoes/HistoricoSugestoes';
const Sugestoes = () => {
const {
sugestoes,
sugestoesArquivadas,
loading,
loadingArchived,
createSugestao,
updateSugestao,
isCreating
} = useSugestoes();
const { isAdmin } = useUserRole();
const [novaSugestao, setNovaSugestao] = useState('');
const [editingNotes, setEditingNotes] = useState<{ [key: string]: string }>({});
const [showHistorico, setShowHistorico] = useState(false);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (novaSugestao.trim()) {
createSugestao(novaSugestao.trim());
setNovaSugestao('');
}
};
const handleStatusChange = (id: string, newStatus: string) => {
const notes = editingNotes[id] || '';
updateSugestao(id, newStatus, notes);
};
const handleNotesChange = (id: string, notes: string) => {
setEditingNotes(prev => ({ ...prev, [id]: notes }));
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'Implementada':
return <CheckCircle className="h-4 w-4 text-green-600" />;
case 'Rejeitada':
return <XCircle className="h-4 w-4 text-red-600" />;
default:
return <Clock className="h-4 w-4 text-yellow-600" />;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'Implementada':
return 'bg-green-50 text-green-700 border-green-200';
case 'Rejeitada':
return 'bg-red-50 text-red-700 border-red-200';
default:
return 'bg-yellow-50 text-yellow-700 border-yellow-200';
}
};
if (loading) {
return (
<div className="container mx-auto p-4 bg-background min-h-screen">
<div className="flex items-center justify-center h-64">
<div className="text-foreground">Carregando sugestões...</div>
</div>
</div>
);
}
return (
<div className="container mx-auto p-2 sm:p-4 space-y-3 sm:space-y-4 bg-background min-h-screen">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<h1 className="text-lg sm:text-2xl lg:text-3xl font-bold text-foreground mb-1">Sugestões de Melhoria</h1>
<p className="text-muted-foreground text-xs sm:text-sm">Compartilhe suas ideias para melhorar o sistema</p>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
onClick={() => setShowHistorico(true)}
className="border-border text-muted-foreground hover:bg-accent h-8 text-xs"
size="sm"
>
<History className="h-3 w-3 mr-1 sm:mr-2" />
<span className="hidden sm:inline">Histórico</span>
<span className="sm:hidden">Hist.</span>
</Button>
<Badge variant="outline" className="text-xs">
{sugestoes.length} {sugestoes.length === 1 ? 'sugestão' : 'sugestões'}
</Badge>
</div>
</div>
{/* Formulário para nova sugestão */}
<Card className="bg-card border-border">
<CardHeader className="pb-2 sm:pb-3">
<CardTitle className="text-card-foreground flex items-center gap-2 text-sm sm:text-lg">
<MessageSquare className="h-4 w-4 sm:h-5 sm:w-5" />
Nova Sugestão
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<form onSubmit={handleSubmit} className="space-y-3">
<div>
<Label htmlFor="sugestao" className="text-muted-foreground text-xs sm:text-sm">
Descreva sua sugestão
</Label>
<Textarea
id="sugestao"
value={novaSugestao}
onChange={(e) => setNovaSugestao(e.target.value)}
placeholder="Descreva sua ideia para melhorar o sistema..."
className="mt-1 bg-background border-border text-foreground placeholder-muted-foreground text-xs sm:text-sm"
rows={3}
required
/>
</div>
<Button
type="submit"
disabled={isCreating || !novaSugestao.trim()}
className="bg-primary hover:bg-primary/90 text-primary-foreground h-8 text-xs"
size="sm"
>
<Send className="h-3 w-3 mr-1 sm:mr-2" />
{isCreating ? 'Enviando...' : 'Enviar Sugestão'}
</Button>
</form>
</CardContent>
</Card>
{/* Lista de sugestões */}
<div className="space-y-2 sm:space-y-3">
{sugestoes.map((sugestao) => (
<Card key={sugestao.id} className="bg-card border-border">
<CardHeader className="pb-2 sm:pb-3">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-2">
<div className="flex flex-col sm:flex-row sm:items-center gap-2">
<div className="flex items-center gap-1 text-muted-foreground text-xs">
<User className="h-3 w-3" />
{sugestao.user_name}
</div>
<div className="flex items-center gap-1 text-muted-foreground text-xs">
<Calendar className="h-3 w-3" />
{format(parseISO(sugestao.created_at), 'dd/MM/yyyy HH:mm', { locale: ptBR })}
</div>
</div>
<Badge variant="outline" className={`${getStatusColor(sugestao.status)} text-xs`}>
{getStatusIcon(sugestao.status)}
<span className="ml-1">{sugestao.status}</span>
</Badge>
</div>
</CardHeader>
<CardContent className="pt-0">
<p className="text-card-foreground mb-3 text-xs sm:text-sm">{sugestao.sugestao}</p>
{/* Painel administrativo */}
{isAdmin && (
<div className="border-t border-border pt-3 space-y-3">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<Label className="text-muted-foreground text-xs">Status</Label>
<Select
value={sugestao.status}
onValueChange={(value) => handleStatusChange(sugestao.id, value)}
>
<SelectTrigger className="bg-background border-border text-foreground h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="Pendente">Pendente</SelectItem>
<SelectItem value="Implementada">Implementada</SelectItem>
<SelectItem value="Rejeitada">Rejeitada</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-muted-foreground text-xs">Observações do Desenvolvedor</Label>
<Input
value={editingNotes[sugestao.id] || sugestao.developer_notes || ''}
onChange={(e) => handleNotesChange(sugestao.id, e.target.value)}
placeholder="Adicionar observações..."
className="bg-background border-border text-foreground placeholder-muted-foreground h-8 text-xs"
/>
</div>
</div>
{sugestao.developer_notes && (
<div className="bg-muted/50 p-2 rounded-lg">
<Label className="text-muted-foreground text-xs font-medium">Observações:</Label>
<p className="text-muted-foreground mt-1 text-xs">{sugestao.developer_notes}</p>
</div>
)}
</div>
)}
</CardContent>
</Card>
))}
</div>
{sugestoes.length === 0 && (
<Card className="bg-card border-border">
<CardContent className="flex flex-col items-center justify-center py-12">
<MessageSquare className="h-12 w-12 text-muted-foreground mb-4" />
<p className="text-muted-foreground text-center">
Nenhuma sugestão foi enviada ainda.{' '}
<br />
Seja o primeiro a compartilhar uma ideia!
</p>
</CardContent>
</Card>
)}
{/* Modal de Histórico */}
<HistoricoSugestoes
isOpen={showHistorico}
onClose={() => setShowHistorico(false)}
sugestoesArquivadas={sugestoesArquivadas}
loading={loadingArchived}
/>
</div>
);
};
export default Sugestoes;
+174
View File
@@ -0,0 +1,174 @@
import { useState, useEffect } from 'react';
import { Plus, History, RefreshCw, AlertCircle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { TaskPanel } from '@/components/tasks/TaskPanel';
import { TaskModal } from '@/components/tasks/TaskModal';
import { useTasksEnhanced } from '@/hooks/useTasksEnhanced';
import { useTasks } from '@/hooks/useTasks';
import { useAuth } from '@/hooks/useAuth';
import { useNavigate } from 'react-router-dom';
const Tarefas = () => {
const navigate = useNavigate();
const { user } = useAuth();
const [showModal, setShowModal] = useState(false);
const [editingTask, setEditingTask] = useState(null);
const { createTask, isCreating, availableUsers: enhancedUsers, availableOFs: enhancedOFs } = useTasksEnhanced();
// Use the existing useTasks hook for legacy support and refetch functionality
const { refetchTasks } = useTasks();
// Use enhanced hook data when available, fallback to legacy hook
const availableUsers = enhancedUsers || [];
const availableOFs = enhancedOFs || [];
// Auto-refresh every 2 minutes
useEffect(() => {
const interval = setInterval(() => {
console.log('🔄 Auto-refreshing tasks...');
refetchTasks();
}, 120000); // 2 minutes
return () => clearInterval(interval);
}, [refetchTasks]);
// Log user authentication status
useEffect(() => {
console.log('👤 Current user:', user?.email || 'Not authenticated');
}, [user]);
const handleCreateTask = () => {
console.log(' Opening task creation modal');
setEditingTask(null);
setShowModal(true);
};
const handleSaveTask = (taskData: any) => {
console.log('💾 Saving task with data:', taskData);
// Add validation to ensure assigned_to is properly set
if (taskData.assigned_to && taskData.assigned_to.length > 0) {
console.log('👥 Task being assigned to users:', taskData.assigned_to);
} else {
console.log('⚠️ Task has no assigned users');
}
createTask(taskData);
setShowModal(false);
setEditingTask(null);
};
const handleManualRefresh = () => {
console.log('🔄 Manual refresh triggered');
refetchTasks();
};
// Show authentication warning if user is not logged in
if (!user) {
return (
<div className="space-y-6 p-4 sm:p-6 bg-background min-h-screen">
<Alert className="bg-destructive/10 border-destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription className="text-destructive">
Você precisa estar logado para visualizar as tarefas.
<Button
variant="link"
className="text-destructive hover:text-destructive/80 p-0 ml-2"
onClick={() => navigate('/auth')}
>
Fazer login
</Button>
</AlertDescription>
</Alert>
</div>
);
}
return (
<div className="space-y-4 sm:space-y-6 p-2 sm:p-4 lg:p-6 bg-background min-h-screen">
{/* Header */}
<div className="space-y-4">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground mb-2">Quadro de Tarefas</h1>
<p className="text-muted-foreground text-sm sm:text-base">
Gerencie todas as suas tarefas ativas e concluídas recentemente, e as que você atribuiu.
{user && <span className="block sm:inline sm:ml-2 text-muted-foreground/80">Logado como: {user.email}</span>}
</p>
</div>
{/* Mobile: Stack buttons vertically */}
<div className="flex flex-col sm:flex-row gap-2 sm:justify-end">
<Button
variant="outline"
onClick={handleManualRefresh}
className="bg-background border-border text-foreground hover:bg-accent w-full sm:w-auto"
>
<RefreshCw className="h-4 w-4 mr-2" />
Atualizar
</Button>
<Button
variant="outline"
onClick={() => navigate('/tarefas/historico')}
className="bg-background border-border text-foreground hover:bg-accent w-full sm:w-auto"
>
<History className="h-4 w-4 mr-2" />
Ver Histórico
</Button>
<Button onClick={handleCreateTask} className="bg-primary hover:bg-primary/90 text-primary-foreground w-full sm:w-auto">
<Plus className="h-4 w-4 mr-2" />
Nova Tarefa
</Button>
</div>
</div>
{/* Task Panels */}
<div className="space-y-6 sm:space-y-8">
{/* Panel 1: My Tasks */}
<TaskPanel
type="my_tasks"
title="Minhas Tarefas"
description="Tarefas atribuídas a você que ainda não foram concluídas"
availableOFs={availableOFs}
availableUsers={availableUsers}
/>
{/* Panel 2: My Completed Tasks */}
<TaskPanel
type="my_completed"
title="Minhas Tarefas Concluídas"
description="Tarefas que você concluiu recentemente"
availableOFs={availableOFs}
availableUsers={availableUsers}
/>
{/* Panel 3: Assigned Tasks */}
<TaskPanel
type="assigned_tasks"
title="Tarefas Atribuídas"
description="Tarefas que você criou e atribuiu a outros usuários"
availableOFs={availableOFs}
availableUsers={availableUsers}
/>
</div>
{/* Task Modal */}
{showModal && (
<TaskModal
task={editingTask}
availableOFs={availableOFs}
availableUsers={availableUsers}
onSave={handleSaveTask}
onClose={() => {
setShowModal(false);
setEditingTask(null);
}}
isLoading={isCreating}
/>
)}
</div>
);
};
export default Tarefas;
+302
View File
@@ -0,0 +1,302 @@
import { useState } from 'react';
import { ArrowLeft, Calendar, Filter, Search } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { TaskCard } from '@/components/tasks/TaskCard';
import { useTasks } from '@/hooks/useTasks';
import { Database } from '@/integrations/supabase/types';
import { useNavigate } from 'react-router-dom';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
type Task = Database['public']['Tables']['tasks']['Row'];
const TarefasHistorico = () => {
const navigate = useNavigate();
const [searchTerm, setSearchTerm] = useState('');
const [filterPriority, setFilterPriority] = useState<string>('all');
const [filterCategory, setFilterCategory] = useState<string>('all');
const { completedTasks, assignedCompletedTasks, isLoading } = useTasks();
// Filter tasks based on search and filters
const filterTasks = (tasks: Task[]) => tasks.filter(task => {
const matchesSearch = searchTerm === '' ||
task.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
task.of_number.toLowerCase().includes(searchTerm.toLowerCase()) ||
(task.description && task.description.toLowerCase().includes(searchTerm.toLowerCase()));
const matchesPriority = filterPriority === 'all' || task.priority === filterPriority;
const matchesCategory = filterCategory === 'all' || task.category === filterCategory;
return matchesSearch && matchesPriority && matchesCategory;
});
const filteredCompletedTasks = filterTasks(completedTasks);
const filteredAssignedTasks = filterTasks(assignedCompletedTasks);
// Group tasks by OF
const groupTasksByOF = (tasks: Task[]) => {
const grouped = tasks.reduce((acc, task) => {
const ofNumber = task.of_number;
if (!acc[ofNumber]) {
acc[ofNumber] = [];
}
acc[ofNumber].push(task);
return acc;
}, {} as Record<string, Task[]>);
// Sort tasks within each OF group by completion date (most recent first)
Object.keys(grouped).forEach(ofNumber => {
grouped[ofNumber].sort((a, b) => {
if (!a.completed_at || !b.completed_at) return 0;
return new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime();
});
});
return grouped;
};
const completedTasksByOF = groupTasksByOF(filteredCompletedTasks);
const assignedTasksByOF = groupTasksByOF(filteredAssignedTasks);
// Get unique categories for filter
const allTasks = [...completedTasks, ...assignedCompletedTasks];
const categories = Array.from(new Set(allTasks.map(task => task.category).filter(Boolean)));
const handleTaskClick = (task: Task) => {
console.log('Task clicked:', task.id);
// TODO: Navigate to task details page
};
if (isLoading) {
return (
<div className="space-y-6 p-4 sm:p-6">
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white mb-2">Histórico de Tarefas</h1>
<p className="text-slate-600 dark:text-slate-400">Carregando histórico...</p>
</div>
</div>
);
}
return (
<div className="space-y-4 sm:space-y-6 p-2 sm:p-4 lg:p-6">
{/* Header */}
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
<Button
variant="outline"
onClick={() => navigate('/tarefas')}
className="bg-slate-50 border-slate-300 text-slate-900 hover:bg-slate-100
dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600 w-fit"
>
<ArrowLeft className="h-4 w-4 mr-2" />
Voltar
</Button>
<div>
<h1 className="text-2xl sm:text-3xl font-bold text-slate-900 dark:text-white mb-2">Histórico de Tarefas</h1>
<p className="text-slate-600 dark:text-slate-400 text-sm sm:text-base">Visualize todas as tarefas concluídas nos últimos 30 dias.</p>
</div>
</div>
</div>
{/* Filters */}
<Card className="bg-slate-100 border-slate-300 dark:bg-slate-800/50 dark:border-slate-700">
<CardHeader className="pb-3">
<CardTitle className="text-slate-800 dark:text-white text-lg">Filtros</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="space-y-2">
<label className="text-slate-700 dark:text-slate-300 text-sm">Buscar</label>
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" />
<Input
placeholder="Título, OF ou descrição..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white border-slate-300 text-slate-900 dark:bg-slate-700 dark:border-slate-600 dark:text-white pl-10"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-slate-700 dark:text-slate-300 text-sm">Prioridade</label>
<Select value={filterPriority} onValueChange={setFilterPriority}>
<SelectTrigger className="bg-white border-slate-300 text-slate-900 dark:bg-slate-700 dark:border-slate-600 dark:text-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-white border-slate-300 dark:bg-slate-700 dark:border-slate-600">
<SelectItem value="all" className="text-slate-900 dark:text-white hover:bg-slate-100 dark:hover:bg-slate-600">Todas</SelectItem>
<SelectItem value="urgente" className="text-slate-900 dark:text-white hover:bg-slate-100 dark:hover:bg-slate-600">Urgente</SelectItem>
<SelectItem value="alta" className="text-slate-900 dark:text-white hover:bg-slate-100 dark:hover:bg-slate-600">Alta</SelectItem>
<SelectItem value="media" className="text-slate-900 dark:text-white hover:bg-slate-100 dark:hover:bg-slate-600">Média</SelectItem>
<SelectItem value="baixa" className="text-slate-900 dark:text-white hover:bg-slate-100 dark:hover:bg-slate-600">Baixa</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-slate-700 dark:text-slate-300 text-sm">Categoria</label>
<Select value={filterCategory} onValueChange={setFilterCategory}>
<SelectTrigger className="bg-white border-slate-300 text-slate-900 dark:bg-slate-700 dark:border-slate-600 dark:text-white">
<SelectValue />
</SelectTrigger>
<SelectContent className="bg-white border-slate-300 dark:bg-slate-700 dark:border-slate-600">
<SelectItem value="all" className="text-slate-900 dark:text-white hover:bg-slate-100 dark:hover:bg-slate-600">Todas</SelectItem>
{categories.map((category) => (
<SelectItem key={category} value={category} className="text-slate-900 dark:text-white hover:bg-slate-100 dark:hover:bg-slate-600">
{category}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button
variant="outline"
onClick={() => {
setSearchTerm('');
setFilterPriority('all');
setFilterCategory('all');
}}
className="bg-white border-slate-300 text-slate-700 hover:bg-slate-100 dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600 w-full"
>
<Filter className="h-4 w-4 mr-2" />
Limpar Filtros
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Tasks History with Tabs */}
<Card className="bg-white border-slate-300 shadow-sm dark:bg-slate-800/50 dark:border-slate-700">
<CardHeader className="pb-3">
<CardTitle className="text-slate-900 dark:text-white">
Histórico de Tarefas
</CardTitle>
</CardHeader>
<CardContent className="pt-0">
<Tabs defaultValue="completed" className="w-full">
<TabsList className="grid w-full grid-cols-2 mb-6 bg-slate-50 border border-slate-300 rounded-md dark:bg-slate-700 dark:border-slate-600">
<TabsTrigger
value="completed"
className="text-slate-700 dark:text-white rounded-md border border-transparent
data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-sm data-[state=active]:border-slate-300
dark:data-[state=active]:bg-slate-800 dark:data-[state=active]:border-slate-600 transition-colors"
>
Tarefas que Conclui ({filteredCompletedTasks.length})
</TabsTrigger>
<TabsTrigger
value="assigned"
className="text-slate-700 dark:text-white rounded-md border border-transparent
data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-sm data-[state=active]:border-slate-300
dark:data-[state=active]:bg-slate-800 dark:data-[state=active]:border-slate-600 transition-colors"
>
Tarefas que Atribui ({filteredAssignedTasks.length})
</TabsTrigger>
</TabsList>
<TabsContent value="completed" className="space-y-4">
{Object.keys(completedTasksByOF).length === 0 ? (
<div className="text-center py-12">
<Calendar className="h-12 w-12 text-slate-500 mx-auto mb-4" />
<p className="text-slate-400 text-lg mb-2">Nenhuma tarefa encontrada</p>
<p className="text-slate-500 text-sm">
{searchTerm || filterPriority !== 'all' || filterCategory !== 'all'
? 'Tente ajustar os filtros para ver mais resultados.'
: 'Não há tarefas que você concluiu nos últimos 30 dias.'
}
</p>
</div>
) : (
<div className="space-y-6">
{Object.entries(completedTasksByOF).map(([ofNumber, tasks]) => (
<div key={ofNumber} className="space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 pb-2 border-b border-slate-300 dark:border-slate-600">
<h3 className="text-slate-900 dark:text-white font-medium">OF: {ofNumber}</h3>
<span className="text-slate-600 dark:text-slate-400 text-sm">
({tasks.length} {tasks.length === 1 ? 'tarefa concluída' : 'tarefas concluídas'})
</span>
</div>
<div className="grid gap-3">
{tasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onEdit={() => {}}
onComplete={() => {}}
onDelete={() => {}}
onClick={handleTaskClick}
onView={handleTaskClick}
isCompleted={true}
/>
))}
</div>
</div>
))}
</div>
)}
</TabsContent>
<TabsContent value="assigned" className="space-y-4">
{Object.keys(assignedTasksByOF).length === 0 ? (
<div className="text-center py-12">
<Calendar className="h-12 w-12 text-slate-500 mx-auto mb-4" />
<p className="text-slate-400 text-lg mb-2">Nenhuma tarefa encontrada</p>
<p className="text-slate-500 text-sm">
{searchTerm || filterPriority !== 'all' || filterCategory !== 'all'
? 'Tente ajustar os filtros para ver mais resultados.'
: 'Não há tarefas que você atribuiu e foram concluídas nos últimos 30 dias.'
}
</p>
</div>
) : (
<div className="space-y-6">
{Object.entries(assignedTasksByOF).map(([ofNumber, tasks]) => (
<div key={ofNumber} className="space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 pb-2 border-b border-slate-300 dark:border-slate-600">
<h3 className="text-slate-900 dark:text-white font-medium">OF: {ofNumber}</h3>
<span className="text-slate-600 dark:text-slate-400 text-sm">
({tasks.length} {tasks.length === 1 ? 'tarefa atribuída concluída' : 'tarefas atribuídas concluídas'})
</span>
</div>
<div className="grid gap-3">
{tasks.map((task) => (
<TaskCard
key={task.id}
task={task}
onEdit={() => {}}
onComplete={() => {}}
onDelete={() => {}}
onClick={handleTaskClick}
onView={handleTaskClick}
isCompleted={true}
showCreator={true}
/>
))}
</div>
</div>
))}
</div>
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
);
};
export default TarefasHistorico;
+192
View File
@@ -0,0 +1,192 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useThemeConfig, ThemeColors } from '@/hooks/useThemeConfig';
import { ThemePreview } from '@/components/theme/ThemePreview';
import { PaletteSelector } from '@/components/theme/PaletteSelector';
import { lightPalettes, darkPalettes, ColorPalette } from '@/constants/themePalettes';
import { toast } from 'sonner';
import { Palette, Save, Loader2 } from 'lucide-react';
const ThemeCustomization = () => {
const { themeConfig, isLoading, saveThemeConfig } = useThemeConfig();
const [selectedLightPalette, setSelectedLightPalette] = useState<ColorPalette | null>(null);
const [selectedDarkPalette, setSelectedDarkPalette] = useState<ColorPalette | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [activeTab, setActiveTab] = useState('light');
useEffect(() => {
if (themeConfig) {
// Encontrar a paleta correspondente ao tema atual
const currentLightPalette = lightPalettes.find(palette =>
JSON.stringify(palette.colors) === JSON.stringify(themeConfig.light_theme)
);
const currentDarkPalette = darkPalettes.find(palette =>
JSON.stringify(palette.colors) === JSON.stringify(themeConfig.dark_theme)
);
setSelectedLightPalette(currentLightPalette || lightPalettes[0]);
setSelectedDarkPalette(currentDarkPalette || darkPalettes[0]);
}
}, [themeConfig]);
const handleSaveChanges = async () => {
if (!selectedLightPalette || !selectedDarkPalette) {
toast.error('Selecione uma paleta para cada modo');
return;
}
setIsSaving(true);
try {
const success = await saveThemeConfig(
selectedLightPalette.colors,
selectedDarkPalette.colors
);
if (success) {
// Aplicar tema atual baseado no modo ativo
const root = document.documentElement;
const isDark = root.classList.contains('dark');
const currentTheme = isDark ? selectedDarkPalette.colors : selectedLightPalette.colors;
Object.entries(currentTheme).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value);
});
}
} catch (error) {
console.error('Error saving theme:', error);
} finally {
setIsSaving(false);
}
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="flex items-center gap-2">
<Loader2 className="h-6 w-6 animate-spin" />
<span>Carregando configurações...</span>
</div>
</div>
);
}
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<Palette className="h-8 w-8 text-primary" />
<div>
<h1 className="text-3xl font-bold">Personalização de Tema</h1>
<p className="text-muted-foreground">
Customize as cores da aplicação para os modos claro e escuro
</p>
</div>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="light">Tema Claro</TabsTrigger>
<TabsTrigger value="dark">Tema Escuro</TabsTrigger>
</TabsList>
<TabsContent value="light" className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Paletas Disponíveis - Modo Claro</CardTitle>
<CardDescription>
Escolha uma paleta de cores para o tema claro
</CardDescription>
</CardHeader>
<CardContent>
<PaletteSelector
palettes={lightPalettes}
selectedPalette={selectedLightPalette}
onSelectPalette={setSelectedLightPalette}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Pré-visualização</CardTitle>
<CardDescription>
Veja como ficará a interface com a paleta selecionada
</CardDescription>
</CardHeader>
<CardContent>
{selectedLightPalette && (
<ThemePreview
colors={selectedLightPalette.colors}
title="Tema Claro"
/>
)}
</CardContent>
</Card>
</div>
</TabsContent>
<TabsContent value="dark" className="space-y-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Paletas Disponíveis - Modo Escuro</CardTitle>
<CardDescription>
Escolha uma paleta de cores para o tema escuro
</CardDescription>
</CardHeader>
<CardContent>
<PaletteSelector
palettes={darkPalettes}
selectedPalette={selectedDarkPalette}
onSelectPalette={setSelectedDarkPalette}
/>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Pré-visualização</CardTitle>
<CardDescription>
Veja como ficará a interface com a paleta selecionada
</CardDescription>
</CardHeader>
<CardContent>
{selectedDarkPalette && (
<ThemePreview
colors={selectedDarkPalette.colors}
title="Tema Escuro"
/>
)}
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
<div className="flex justify-end">
<Button
onClick={handleSaveChanges}
disabled={isSaving || !selectedLightPalette || !selectedDarkPalette}
className="min-w-[150px]"
>
{isSaving ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Salvando...
</>
) : (
<>
<Save className="h-4 w-4 mr-2" />
Salvar Alterações
</>
)}
</Button>
</div>
</div>
);
};
export default ThemeCustomization;
+198
View File
@@ -0,0 +1,198 @@
import { useState, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { Label } from '@/components/ui/label';
import { ArrowLeft, Palette, Save, Sparkles } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { useThemeConfig, ThemeColors } from '@/hooks/useThemeConfig';
import { useIconStyle, IconStyleType } from '@/hooks/useIconStyle';
import { ThemePreview } from '@/components/theme/ThemePreview';
import { PaletteSelector } from '@/components/theme/PaletteSelector';
import { COLOR_PALETTES, ColorPalette } from '@/constants/themePalettes';
const ICON_STYLE_OPTIONS = [
{
value: 'white' as IconStyleType,
label: 'Ícones Brancos',
description: 'Ícones em cor branca padrão'
},
{
value: 'themed' as IconStyleType,
label: 'Ícones Temáticos',
description: 'Ícones na cor da paleta selecionada'
},
{
value: 'colorful' as IconStyleType,
label: 'Ícones Coloridos',
description: 'Ícones com cores individuais (recomendado)'
}
];
const ThemeCustomizationPage = () => {
const navigate = useNavigate();
const { themeConfig, isLoading, saveThemeConfig } = useThemeConfig();
const { iconStyle, setIconStyle } = useIconStyle();
const [lightTheme, setLightTheme] = useState<ThemeColors | null>(null);
const [darkTheme, setDarkTheme] = useState<ThemeColors | null>(null);
const [selectedPalette, setSelectedPalette] = useState<ColorPalette | null>(null);
const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
if (themeConfig) {
setLightTheme(themeConfig.light_theme);
setDarkTheme(themeConfig.dark_theme);
}
}, [themeConfig]);
const handlePaletteSelect = (palette: ColorPalette) => {
setSelectedPalette(palette);
setLightTheme(palette.colors);
setDarkTheme(palette.darkColors || palette.colors);
};
const handleSave = async () => {
if (!lightTheme || !darkTheme) {
toast.error('Por favor, selecione uma paleta de cores');
return;
}
setIsSaving(true);
try {
const success = await saveThemeConfig(lightTheme, darkTheme);
if (success) {
toast.success('Personalizações salvas com sucesso!');
}
} catch (error) {
console.error('Error saving theme:', error);
toast.error('Erro ao salvar personalizações');
} finally {
setIsSaving(false);
}
};
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
return (
<div className="min-h-screen bg-background p-6">
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button
variant="outline"
size="sm"
onClick={() => navigate('/configuracoes')}
className="bg-card border-border text-foreground hover:bg-accent"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar
</Button>
<div>
<h1 className="text-3xl font-bold text-foreground mb-2">Personalização de Tema</h1>
<p className="text-muted-foreground">Customize as cores e ícones da aplicação</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Painel de Seleção */}
<div className="space-y-6">
{/* Seleção de Paleta de Cores */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center gap-2">
<Palette className="h-5 w-5" />
Paleta de Cores
</CardTitle>
</CardHeader>
<CardContent>
<PaletteSelector
palettes={COLOR_PALETTES}
selectedPalette={selectedPalette}
onSelectPalette={handlePaletteSelect}
/>
</CardContent>
</Card>
{/* Seleção de Estilo de Ícones */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground flex items-center gap-2">
<Sparkles className="h-5 w-5" />
Estilo dos Ícones
</CardTitle>
</CardHeader>
<CardContent>
<RadioGroup value={iconStyle} onValueChange={setIconStyle} className="space-y-4">
{ICON_STYLE_OPTIONS.map((option) => (
<div key={option.value} className="flex items-start space-x-3 p-3 rounded-lg border border-border hover:border-accent transition-colors">
<RadioGroupItem value={option.value} id={option.value} className="mt-1" />
<div className="flex-1">
<Label htmlFor={option.value} className="text-foreground font-medium cursor-pointer">
{option.label}
</Label>
<p className="text-muted-foreground text-sm mt-1">{option.description}</p>
</div>
</div>
))}
</RadioGroup>
</CardContent>
</Card>
{/* Botão Salvar */}
<Button
onClick={handleSave}
disabled={isSaving || !lightTheme || !darkTheme}
className="w-full bg-primary hover:bg-primary/90 text-primary-foreground"
>
<Save className="w-4 h-4 mr-2" />
{isSaving ? 'Salvando...' : 'Salvar Personalizações'}
</Button>
</div>
{/* Painel de Preview */}
<Card className="bg-card border-border">
<CardHeader>
<CardTitle className="text-foreground">Pré-visualização</CardTitle>
</CardHeader>
<CardContent>
<Tabs defaultValue="light" className="w-full">
<TabsList className="grid w-full grid-cols-2 bg-muted">
<TabsTrigger value="light" className="text-foreground data-[state=active]:bg-background">
Tema Claro
</TabsTrigger>
<TabsTrigger value="dark" className="text-foreground data-[state=active]:bg-background">
Tema Escuro
</TabsTrigger>
</TabsList>
<TabsContent value="light" className="mt-4">
{lightTheme && (
<ThemePreview colors={lightTheme} title="Claro" />
)}
</TabsContent>
<TabsContent value="dark" className="mt-4">
{darkTheme && (
<ThemePreview colors={darkTheme} title="Escuro" />
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
</div>
</div>
</div>
);
};
export default ThemeCustomizationPage;
+343
View File
@@ -0,0 +1,343 @@
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Users, UserPlus, Shield, Settings, Mail, Activity } from 'lucide-react';
import { UsersTable } from '@/components/users/UsersTable';
import { PendingUsersTable } from '@/components/users/PendingUsersTable';
import { FunctionsManager } from '@/components/users/FunctionsManager';
import { PrivilegesManager } from '@/components/users/PrivilegesManager';
import { PasswordResetRequests } from '@/components/users/PasswordResetRequests';
import { SessionLogsSimple } from '@/components/users/SessionLogsSimple';
import { UserModal } from '@/components/users/UserModal';
import { useUserManagement, UserProfile } from '@/hooks/useUserManagement';
import { usePermissionControl } from '@/hooks/usePermissionControl';
const UserManagement = () => {
const [isUserModalOpen, setIsUserModalOpen] = useState(false);
const [selectedUser, setSelectedUser] = useState<UserProfile | null>(null);
const [activeTab, setActiveTab] = useState('users');
const {
canView,
canCreate,
canEdit,
canDelete,
canAdmin,
isDisabled,
canInteractWithSpecialMenus
} = usePermissionControl();
const {
users,
pendingUsers,
functions,
privileges,
createUser,
approveUser,
rejectUser,
createFunction,
updateFunction,
deleteFunction,
createPrivilege: createPrivilegeBase,
updatePrivilege: updatePrivilegeBase,
deletePrivilege,
updateUser,
toggleUserStatus,
deleteUser,
canDeleteUser,
getUserDependencies
} = useUserManagement();
// Verificar se pode acessar esta página - somente admins ou usuários com permissões especiais
if (!canView() || !canInteractWithSpecialMenus()) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center space-y-4">
<div className="text-6xl">🔒</div>
<h2 className="text-2xl font-semibold text-muted-foreground">
Acesso Negado
</h2>
<p className="text-muted-foreground max-w-md">
Você não tem permissão para acessar o gerenciamento de usuários.
</p>
</div>
</div>
);
}
// Wrapper functions to match expected signatures
const createPrivilege = async (data: { name: string; description?: string; permissions: Record<string, boolean> }): Promise<void> => {
await createPrivilegeBase(data);
};
const updatePrivilege = async (id: string, data: { name: string; description?: string; permissions: Record<string, boolean> }): Promise<void> => {
await updatePrivilegeBase(id, data);
};
const handleEditUser = (user: UserProfile) => {
if (!canEdit()) return;
setSelectedUser(user);
setIsUserModalOpen(true);
};
const handleCreateUser = () => {
if (!canCreate()) return;
setSelectedUser(null);
setIsUserModalOpen(true);
};
const handleToggleStatus = (userId: string) => {
if (!canEdit()) return;
const user = users.find(u => u.id === userId);
if (user) {
toggleUserStatus(userId, user.status);
}
};
const handleDeleteUser = async (userId: string, replaceReferences: boolean = false) => {
if (!canDelete()) return;
return await deleteUser(userId, replaceReferences);
};
const handleApproveUser = (userId: string) => {
if (!canEdit()) return;
approveUser(userId);
};
const handleRejectUser = (userId: string) => {
if (!canDelete()) return;
rejectUser(userId);
};
const handleSaveUser = (userId: string, data: Partial<UserProfile>) => {
if (!canEdit()) return;
updateUser(userId, data);
setSelectedUser(null);
setIsUserModalOpen(false);
};
const handleCreateNewUser = async (data: {
email: string;
full_name?: string;
function_id?: string;
privilege_id?: string;
}) => {
if (!canCreate()) return;
try {
await createUser(data);
setIsUserModalOpen(false);
} catch (error) {
// Error is already handled in the hook
}
};
const handleCloseModal = () => {
setSelectedUser(null);
setIsUserModalOpen(false);
};
return (
<div className="space-y-4 md:space-y-6 p-2 md:p-0">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold text-foreground">Usuários e Privilégios</h1>
<p className="text-sm md:text-base text-muted-foreground">Gerenciamento de usuários, funções e privilégios do sistema</p>
</div>
{canCreate() && (
<Button
onClick={handleCreateUser}
className="mobile-full-width"
disabled={isDisabled('create')}
>
<UserPlus className="w-4 h-4 mr-2" />
<span className="text-sm md:text-base">Novo Usuário</span>
</Button>
)}
</div>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<TabsList className="grid w-full grid-cols-2 md:grid-cols-6 bg-muted h-auto p-1">
<TabsTrigger
value="users"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Users className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Usuários</span>
<span className="sm:hidden">Users</span>
</TabsTrigger>
{canEdit() && (
<TabsTrigger
value="pending"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<UserPlus className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Pendentes</span>
<span className="sm:hidden">Pend</span>
</TabsTrigger>
)}
{canAdmin() && (
<TabsTrigger
value="password-reset"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Mail className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Redefinições</span>
<span className="sm:hidden">Reset</span>
</TabsTrigger>
)}
{canEdit() && (
<TabsTrigger
value="functions"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Settings className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Funções</span>
<span className="sm:hidden">Func</span>
</TabsTrigger>
)}
{canEdit() && (
<TabsTrigger
value="privileges"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Shield className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Privilégios</span>
<span className="sm:hidden">Priv</span>
</TabsTrigger>
)}
{canView() && (
<TabsTrigger
value="session-logs"
className="flex items-center gap-1 md:gap-2 text-xs md:text-sm p-2 md:p-3"
>
<Activity className="h-3 w-3 md:h-4 md:w-4" />
<span className="hidden sm:inline">Logs de Sessão</span>
<span className="sm:hidden">Logs</span>
</TabsTrigger>
)}
</TabsList>
<TabsContent value="users" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Users className="h-4 w-4 md:h-5 md:w-5" />
Usuários Ativos
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<div className="overflow-x-auto custom-scrollbar">
<UsersTable
users={users}
functions={functions}
privileges={privileges}
onEditUser={canEdit() ? handleEditUser : undefined}
onToggleStatus={canEdit() ? handleToggleStatus : undefined}
onDeleteUser={canDelete() ? handleDeleteUser : undefined}
canDeleteUser={canDeleteUser}
getUserDependencies={getUserDependencies}
/>
</div>
</CardContent>
</Card>
</TabsContent>
{canEdit() && (
<TabsContent value="pending" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<UserPlus className="h-4 w-4 md:h-5 md:w-5" />
Usuários Pendentes
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<div className="overflow-x-auto custom-scrollbar">
<PendingUsersTable
users={pendingUsers}
onApprove={handleApproveUser}
onReject={handleRejectUser}
/>
</div>
</CardContent>
</Card>
</TabsContent>
)}
{canAdmin() && (
<TabsContent value="password-reset" className="space-y-4 mt-4">
<PasswordResetRequests />
</TabsContent>
)}
{canEdit() && (
<TabsContent value="functions" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Settings className="h-4 w-4 md:h-5 md:w-5" />
Gerenciar Funções
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<FunctionsManager
functions={functions}
onCreate={canCreate() ? createFunction : undefined}
onUpdate={canEdit() ? updateFunction : undefined}
onDelete={canDelete() ? deleteFunction : undefined}
/>
</CardContent>
</Card>
</TabsContent>
)}
{canEdit() && (
<TabsContent value="privileges" className="space-y-4 mt-4">
<Card className="card-mobile">
<CardHeader className="card-header-mobile">
<CardTitle className="text-lg md:text-xl flex items-center gap-2">
<Shield className="h-4 w-4 md:h-5 md:w-5" />
Gerenciar Privilégios
</CardTitle>
</CardHeader>
<CardContent className="card-content-mobile">
<PrivilegesManager
privileges={privileges}
onCreate={canCreate() ? createPrivilege : undefined}
onUpdate={canEdit() ? updatePrivilege : undefined}
onDelete={canDelete() ? deletePrivilege : undefined}
/>
</CardContent>
</Card>
</TabsContent>
)}
{canView() && (
<TabsContent value="session-logs" className="space-y-4 mt-4">
<SessionLogsSimple users={users} />
</TabsContent>
)}
</Tabs>
{isUserModalOpen && (
<UserModal
user={selectedUser}
functions={functions}
privileges={privileges}
onSave={canEdit() ? handleSaveUser : undefined}
onCreate={canCreate() ? handleCreateNewUser : undefined}
onClose={handleCloseModal}
readOnly={!canEdit() && !canCreate()}
/>
)}
</div>
);
};
export default UserManagement;
+330
View File
@@ -0,0 +1,330 @@
import React, { useState, useEffect } from 'react';
import { StandardPageLayout } from '@/components/layout/StandardPageLayout';
import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Search, Eye, Download, FileText, AlertTriangle, CheckCircle } from 'lucide-react';
import { useOFsAtivas } from '@/hooks/useOFsAtivas';
import { useAuditoriaInconsistencias } from '@/hooks/useAuditoriaInconsistencias';
import { InconsistenciaDetalhesModal } from '@/components/auditoria/InconsistenciaDetalhesModal';
import { generateProfessionalPDF } from '@/utils/pdfGenerator';
import { toast } from '@/hooks/use-toast';
import { supabase } from '@/integrations/supabase/client';
const VerInconsistencias = () => {
const [selectedOF, setSelectedOF] = useState<string>('');
const [selectedFase, setSelectedFase] = useState<string>('');
const [selectedProcesso, setSelectedProcesso] = useState<string>('todas');
const [fasesDaOF, setFasesDaOF] = useState<string[]>([]);
const [detalhesModal, setDetalhesModal] = useState<{ open: boolean; inconsistencia: any }>({
open: false,
inconsistencia: null
});
const { data: ofs = [] } = useOFsAtivas();
const {
data: resultadoAuditoria,
loading,
executarAuditoria
} = useAuditoriaInconsistencias();
const processos = ['Corte', 'Solda', 'Pintura', 'Expedição'];
// Buscar fases da OF selecionada
useEffect(() => {
const buscarFasesDaOF = async () => {
if (!selectedOF) {
setFasesDaOF([]);
setSelectedFase('');
return;
}
try {
const { data, error } = await supabase
.from('pecas')
.select('etapa_fase')
.eq('of_number', selectedOF)
.not('etapa_fase', 'is', null);
if (error) {
console.error('Erro ao buscar fases da OF:', error);
return;
}
const fasesUnicas = [...new Set(data.map(p => p.etapa_fase))]
.filter((fase): fase is string => Boolean(fase))
.sort();
setFasesDaOF(fasesUnicas);
// Limpar seleção de fase se não existir mais
if (selectedFase && !fasesUnicas.includes(selectedFase)) {
setSelectedFase('');
}
} catch (error) {
console.error('Erro ao buscar fases:', error);
}
};
buscarFasesDaOF();
}, [selectedOF, selectedFase]);
const handleChecarInconsistencias = () => {
if (!selectedOF) {
toast({
title: 'OF obrigatória',
description: 'Selecione uma OF para verificar inconsistências',
variant: 'destructive'
});
return;
}
if (!selectedFase) {
toast({
title: 'Fase obrigatória',
description: 'Selecione uma fase da OF para verificar inconsistências',
variant: 'destructive'
});
return;
}
executarAuditoria(selectedOF, selectedProcesso === 'todas' ? undefined : selectedProcesso, selectedFase);
};
const handleExportarPDF = async () => {
if (!resultadoAuditoria) return;
try {
await generateProfessionalPDF('auditoria-inconsistencias', 'auditoria-inconsistencias.pdf');
toast({
title: 'PDF gerado',
description: 'Relatório de inconsistências exportado com sucesso'
});
} catch (error) {
toast({
title: 'Erro ao gerar PDF',
description: 'Não foi possível exportar o relatório',
variant: 'destructive'
});
}
};
const getTotalInconsistencias = () => {
if (!resultadoAuditoria) return 0;
return Object.values(resultadoAuditoria.inconsistencias).reduce((total, categoria: any) => total + categoria.length, 0);
};
return (
<StandardPageLayout
title="Ver Inconsistências"
subtitle="Ferramenta de auditoria para verificar inconsistências no sistema"
>
<div className="space-y-6">
{/* Filtros */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Search className="h-5 w-5" />
Parâmetros de Auditoria
</CardTitle>
<CardDescription>
Selecione a OF e fase para verificar inconsistências
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="text-sm font-medium mb-2 block">Ordem de Fabricação</label>
<Select value={selectedOF} onValueChange={setSelectedOF}>
<SelectTrigger>
<SelectValue placeholder="Selecione uma OF" />
</SelectTrigger>
<SelectContent>
{ofs.map((of) => (
<SelectItem key={of.of_number} value={of.of_number}>
{of.of_number} - {of.descricao_resumida || 'Sem descrição'}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Fase</label>
<Select value={selectedFase} onValueChange={setSelectedFase} disabled={!selectedOF}>
<SelectTrigger>
<SelectValue placeholder={selectedOF ? "Selecione uma fase" : "Selecione uma OF primeiro"} />
</SelectTrigger>
<SelectContent>
{fasesDaOF.map((fase) => (
<SelectItem key={fase} value={fase}>
{fase}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium mb-2 block">Processo</label>
<Select value={selectedProcesso} onValueChange={setSelectedProcesso}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="todas">Todos os Processos</SelectItem>
{processos.map((processo) => (
<SelectItem key={processo} value={processo}>
{processo}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end">
<Button
onClick={handleChecarInconsistencias}
disabled={loading || !selectedOF || !selectedFase}
className="w-full"
>
{loading ? 'Verificando...' : 'Checar Inconsistências'}
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Resultados */}
{resultadoAuditoria && (
<div id="auditoria-inconsistencias" className="space-y-6">
{/* Resumo */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
{getTotalInconsistencias() > 0 ? (
<AlertTriangle className="h-5 w-5 text-amber-500" />
) : (
<CheckCircle className="h-5 w-5 text-green-500" />
)}
Resultado da Auditoria
</CardTitle>
<CardDescription>
OF: {selectedOF} | Fase: {selectedFase} | Processo: {selectedProcesso === 'todas' ? 'Todos' : selectedProcesso}
</CardDescription>
</div>
<div className="flex gap-2">
<Button onClick={handleExportarPDF} variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Exportar PDF
</Button>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-red-600">
{resultadoAuditoria.inconsistencias.processos?.length || 0}
</div>
<div className="text-sm text-muted-foreground">Processos</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-orange-600">
{resultadoAuditoria.inconsistencias.quantidades?.length || 0}
</div>
<div className="text-sm text-muted-foreground">Quantidades</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-yellow-600">
{resultadoAuditoria.inconsistencias.expedicao?.length || 0}
</div>
<div className="text-sm text-muted-foreground">Expedição</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600">
{resultadoAuditoria.inconsistencias.prioridades?.length || 0}
</div>
<div className="text-sm text-muted-foreground">Prioridades</div>
</div>
</div>
</CardContent>
</Card>
{/* Inconsistências por Categoria */}
{Object.entries(resultadoAuditoria.inconsistencias).map(([categoria, inconsistencias]) => (
<Card key={categoria}>
<CardHeader>
<CardTitle className="capitalize">
Inconsistências de {categoria}
<Badge variant="secondary" className="ml-2">
{(inconsistencias as any[]).length}
</Badge>
</CardTitle>
</CardHeader>
<CardContent>
{(inconsistencias as any[]).length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
<CheckCircle className="h-12 w-12 mx-auto mb-2 text-green-500" />
Nenhuma inconsistência encontrada nesta categoria
</div>
) : (
<div className="space-y-2">
{(inconsistencias as any[]).map((item, index) => (
<div key={index} className="flex items-center justify-between p-3 border rounded-lg">
<div className="flex-1">
<div className="font-medium">{item.descricao}</div>
<div className="text-sm text-muted-foreground">
Marca: {item.marca} | Tipo: {item.tipo}
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setDetalhesModal({ open: true, inconsistencia: item })}
>
<Eye className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</CardContent>
</Card>
))}
{/* Verificações Concluídas */}
<Card>
<CardHeader>
<CardTitle>Verificações Realizadas</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
{resultadoAuditoria.verificacoesRealizadas.map((verificacao, index) => (
<div key={index} className="flex items-center gap-2 text-sm">
<CheckCircle className="h-4 w-4 text-green-500" />
{verificacao}
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
{/* Modal de Detalhes */}
<InconsistenciaDetalhesModal
open={detalhesModal.open}
onOpenChange={(open) => setDetalhesModal({ open, inconsistencia: null })}
inconsistencia={detalhesModal.inconsistencia}
/>
</div>
</StandardPageLayout>
);
};
export default VerInconsistencias;
+40
View File
@@ -0,0 +1,40 @@
export { default as Auth } from './Auth';
export { default as Dashboard } from './Dashboard';
export { default as CadastroOF } from './CadastroOF';
export { default as CadastroPecas } from './CadastroPecas';
export { default as SeletorOF } from './SeletorOF';
export { default as CadastroPecasFiltrado } from './CadastroPecasFiltrado';
export { default as Equipamentos } from './Equipamentos';
export { default as Estoque } from './Estoque';
export { default as SolicitacaoCompras } from './SolicitacaoCompras';
export { default as OrdensFabricacao } from './OrdensFabricacao';
export { default as OFsConcluidas } from './OFsConcluidas';
export { default as CronogramaOF } from './CronogramaOF';
export { default as Producao } from './Producao';
export { default as DiarioProducao } from './DiarioProducao';
export { default as ApontamentoProducao } from './ApontamentoProducao';
export { default as DashboardProducao } from './DashboardProducao';
export { default as Expedicao } from './Expedicao';
export { default as Obra } from './Obra';
export { default as ObraConfiguracoes } from './ObraConfiguracoes';
export { default as Tarefas } from './Tarefas';
export { default as TarefasHistorico } from './TarefasHistorico';
export { default as Sistema } from './Sistema';
export { default as Configuracoes } from './Configuracoes';
export { default as Admin } from './Admin';
export { default as UserManagement } from './UserManagement';
export { default as ThemeCustomizationPage } from './ThemeCustomizationPage';
export { default as NotFound } from './NotFound';
export { default as Catalogos } from './Catalogos';
export { default as BibliotecaFerramentas } from './BibliotecaFerramentas';
export { default as BibliotecaNormas } from './BibliotecaNormas';
export { default as BibliotecaReferencias } from './BibliotecaReferencias';
export { default as Sugestoes } from './Sugestoes';
export { default as ConversoresDados } from './ConversoresDados';
export { default as PlanejamentoProducao } from './PlanejamentoProducao';
export { default as PainelIndustrial } from './PainelIndustrial';
export { default as PrioridadesFabricacao } from './PrioridadesFabricacao';
export { default as MapaInterativo } from './MapaInterativo';
export { default as Atribuicoes } from './Atribuicoes';
export { default as VerInconsistencias } from './VerInconsistencias';