🚀 Initial commit: Versão atual do TrackSteel APP
This commit is contained in:
@@ -0,0 +1,433 @@
|
||||
import React, { 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 { Calendar } from '@/components/ui/calendar';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { CalendarIcon, Save, Plus, Trash2, ChevronUp, ChevronDown } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CronogramaOf, ProcessoCronograma } from '@/types/cronograma';
|
||||
import { useCronogramaOperations } from '@/hooks/useCronogramaOperations';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CronogramaFormProps {
|
||||
cronograma?: CronogramaOf | null;
|
||||
onClose: () => void;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const processosDefault = [
|
||||
'Detalhamento',
|
||||
'Fabricação',
|
||||
'Pint/Galv',
|
||||
'Instalação',
|
||||
'Aceite/DB'
|
||||
];
|
||||
|
||||
export const CronogramaForm: React.FC<CronogramaFormProps> = ({ cronograma, onClose, isOpen }) => {
|
||||
const { saveCronograma, getCronogramaPorOf } = useCronogramaOperations();
|
||||
const [formData, setFormData] = useState({
|
||||
of_id: '',
|
||||
gestor_id: '',
|
||||
revisao: 1,
|
||||
processos: processosDefault.map((nome, index) => ({
|
||||
nome_processo: nome,
|
||||
data_inicio: '',
|
||||
data_fim: '',
|
||||
ordem: index + 1
|
||||
})) as ProcessoCronograma[]
|
||||
});
|
||||
|
||||
const [ofs, setOfs] = useState<any[]>([]);
|
||||
const [gestores, setGestores] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
loadOfs();
|
||||
loadGestores();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cronograma) {
|
||||
console.log('Carregando cronograma para edição:', cronograma);
|
||||
setFormData({
|
||||
of_id: cronograma.of_id,
|
||||
gestor_id: cronograma.gestor_id,
|
||||
revisao: cronograma.revisao,
|
||||
processos: cronograma.processos.length > 0 ? cronograma.processos.map(p => ({
|
||||
id: p.id,
|
||||
nome_processo: p.nome_processo,
|
||||
data_inicio: p.data_inicio,
|
||||
data_fim: p.data_fim,
|
||||
ordem: p.ordem
|
||||
})) : processosDefault.map((nome, index) => ({
|
||||
nome_processo: nome,
|
||||
data_inicio: '',
|
||||
data_fim: '',
|
||||
ordem: index + 1
|
||||
}))
|
||||
});
|
||||
}
|
||||
}, [cronograma]);
|
||||
|
||||
const loadOfs = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('ordens_fabricacao')
|
||||
.select('id, num_of, descritivo')
|
||||
.order('num_of');
|
||||
|
||||
if (error) throw error;
|
||||
setOfs(data || []);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar OFs:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadGestores = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('profiles')
|
||||
.select('id, full_name, email')
|
||||
.not('full_name', 'is', null)
|
||||
.order('full_name');
|
||||
|
||||
if (error) throw error;
|
||||
setGestores(data || []);
|
||||
} catch (error) {
|
||||
console.error('Erro ao carregar gestores:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOfChange = async (ofId: string) => {
|
||||
console.log('OF selecionada:', ofId);
|
||||
setFormData(prev => ({ ...prev, of_id: ofId }));
|
||||
|
||||
// Verificar se já existe cronograma para esta OF
|
||||
const cronogramaExistente = await getCronogramaPorOf(ofId);
|
||||
console.log('Cronograma existente encontrado:', cronogramaExistente);
|
||||
|
||||
if (cronogramaExistente) {
|
||||
// Carregar dados do cronograma existente
|
||||
setFormData({
|
||||
of_id: ofId,
|
||||
gestor_id: cronogramaExistente.gestor_id,
|
||||
revisao: cronogramaExistente.revisao,
|
||||
processos: cronogramaExistente.processos.length > 0 ? cronogramaExistente.processos.map(p => ({
|
||||
id: p.id,
|
||||
nome_processo: p.nome_processo,
|
||||
data_inicio: p.data_inicio,
|
||||
data_fim: p.data_fim,
|
||||
ordem: p.ordem
|
||||
})) : processosDefault.map((nome, index) => ({
|
||||
nome_processo: nome,
|
||||
data_inicio: '',
|
||||
data_fim: '',
|
||||
ordem: index + 1
|
||||
}))
|
||||
});
|
||||
toast.info('Cronograma existente carregado para edição');
|
||||
} else {
|
||||
// Resetar para valores padrão se não houver cronograma
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
gestor_id: '',
|
||||
revisao: 1,
|
||||
processos: processosDefault.map((nome, index) => ({
|
||||
nome_processo: nome,
|
||||
data_inicio: '',
|
||||
data_fim: '',
|
||||
ordem: index + 1
|
||||
}))
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleProcessoChange = (index: number, field: keyof ProcessoCronograma, value: string) => {
|
||||
const newProcessos = [...formData.processos];
|
||||
newProcessos[index] = { ...newProcessos[index], [field]: value };
|
||||
setFormData({ ...formData, processos: newProcessos });
|
||||
};
|
||||
|
||||
const addProcesso = () => {
|
||||
const newProcesso: ProcessoCronograma = {
|
||||
nome_processo: '',
|
||||
data_inicio: '',
|
||||
data_fim: '',
|
||||
ordem: formData.processos.length + 1
|
||||
};
|
||||
setFormData({
|
||||
...formData,
|
||||
processos: [...formData.processos, newProcesso]
|
||||
});
|
||||
};
|
||||
|
||||
const removeProcesso = (index: number) => {
|
||||
const newProcessos = formData.processos.filter((_, i) => i !== index);
|
||||
// Reordenar
|
||||
newProcessos.forEach((processo, i) => {
|
||||
processo.ordem = i + 1;
|
||||
});
|
||||
setFormData({ ...formData, processos: newProcessos });
|
||||
};
|
||||
|
||||
const moveProcessoUp = (index: number) => {
|
||||
if (index === 0) return;
|
||||
|
||||
const newProcessos = [...formData.processos];
|
||||
[newProcessos[index], newProcessos[index - 1]] = [newProcessos[index - 1], newProcessos[index]];
|
||||
|
||||
// Atualizar ordem
|
||||
newProcessos.forEach((processo, i) => {
|
||||
processo.ordem = i + 1;
|
||||
});
|
||||
|
||||
setFormData({ ...formData, processos: newProcessos });
|
||||
};
|
||||
|
||||
const moveProcessoDown = (index: number) => {
|
||||
if (index === formData.processos.length - 1) return;
|
||||
|
||||
const newProcessos = [...formData.processos];
|
||||
[newProcessos[index], newProcessos[index + 1]] = [newProcessos[index + 1], newProcessos[index]];
|
||||
|
||||
// Atualizar ordem
|
||||
newProcessos.forEach((processo, i) => {
|
||||
processo.ordem = i + 1;
|
||||
});
|
||||
|
||||
setFormData({ ...formData, processos: newProcessos });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.of_id || !formData.gestor_id) {
|
||||
toast.error('Por favor, selecione a OF e o gestor responsável');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.processos.some(p => !p.nome_processo || !p.data_inicio || !p.data_fim)) {
|
||||
toast.error('Por favor, preencha todos os campos dos processos');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
const success = await saveCronograma({
|
||||
...formData,
|
||||
id: cronograma?.id
|
||||
});
|
||||
setLoading(false);
|
||||
|
||||
if (success) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const DatePicker = ({ date, onDateChange, placeholder }: {
|
||||
date: string;
|
||||
onDateChange: (date: string) => void;
|
||||
placeholder: string;
|
||||
}) => {
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
|
||||
date ? new Date(date + 'T12:00:00') : undefined
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"w-full justify-start text-left font-normal bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600 text-gray-900 dark:text-gray-100",
|
||||
!selectedDate && "text-gray-500 dark:text-gray-400"
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{selectedDate ? format(selectedDate, "dd/MM/yyyy", { locale: ptBR }) : placeholder}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0 bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-600" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={(date) => {
|
||||
if (date) {
|
||||
setSelectedDate(date);
|
||||
onDateChange(format(date, 'yyyy-MM-dd'));
|
||||
}
|
||||
}}
|
||||
initialFocus
|
||||
className="pointer-events-auto p-3"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{cronograma ? 'Editar Cronograma' : 'Novo Cronograma'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="of_id">Ordem de Fabricação *</Label>
|
||||
<Select
|
||||
value={formData.of_id}
|
||||
onValueChange={handleOfChange}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione a OF" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ofs.map((of) => (
|
||||
<SelectItem key={of.id} value={of.id}>
|
||||
{of.num_of} - {of.descritivo}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gestor_id">Gestor Responsável *</Label>
|
||||
<Select
|
||||
value={formData.gestor_id}
|
||||
onValueChange={(value) => setFormData({ ...formData, gestor_id: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Selecione o gestor" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{gestores.map((gestor) => (
|
||||
<SelectItem key={gestor.id} value={gestor.id}>
|
||||
{gestor.full_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Processos do Cronograma</h3>
|
||||
<Button type="button" onClick={addProcesso} size="sm" variant="outline">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Adicionar Processo
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{formData.processos.map((processo, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="flex items-start gap-4">
|
||||
{/* Controles de ordenação */}
|
||||
<div className="flex flex-col gap-1 mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => moveProcessoUp(index)}
|
||||
disabled={index === 0}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ChevronUp className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => moveProcessoDown(index)}
|
||||
disabled={index === formData.processos.length - 1}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Número de ordem */}
|
||||
<div className="flex items-center justify-center w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full mt-6">
|
||||
<span className="text-sm font-semibold text-blue-600 dark:text-blue-300">
|
||||
{index + 1}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Campos do processo */}
|
||||
<div className="flex-1">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 items-end">
|
||||
<div className="space-y-2">
|
||||
<Label>Nome do Processo *</Label>
|
||||
<Input
|
||||
value={processo.nome_processo}
|
||||
onChange={(e) => handleProcessoChange(index, 'nome_processo', e.target.value)}
|
||||
placeholder="Ex: Detalhamento"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Data de Início *</Label>
|
||||
<DatePicker
|
||||
date={processo.data_inicio}
|
||||
onDateChange={(date) => handleProcessoChange(index, 'data_inicio', date)}
|
||||
placeholder="Selecionar data"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Data de Fim *</Label>
|
||||
<DatePicker
|
||||
date={processo.data_fim}
|
||||
onDateChange={(date) => handleProcessoChange(index, 'data_fim', date)}
|
||||
placeholder="Selecionar data"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Botão de remoção */}
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => removeProcesso(index)}
|
||||
disabled={formData.processos.length === 1}
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{loading ? 'Salvando...' : 'Salvar Cronograma'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user