🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user