🚀 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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,185 @@
|
||||
|
||||
import React from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { X, Calendar, FileDown } from 'lucide-react';
|
||||
import { format, differenceInDays, parseISO } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { CronogramaOf } from '@/types/cronograma';
|
||||
|
||||
interface CronogramaGanttProps {
|
||||
cronograma: CronogramaOf;
|
||||
onClose: () => void;
|
||||
onExportPDF?: () => void;
|
||||
}
|
||||
|
||||
export const CronogramaGantt: React.FC<CronogramaGanttProps> = ({ cronograma, onClose, onExportPDF }) => {
|
||||
const cores = [
|
||||
'bg-blue-500 dark:bg-blue-600',
|
||||
'bg-green-500 dark:bg-green-600',
|
||||
'bg-yellow-500 dark:bg-yellow-600',
|
||||
'bg-purple-500 dark:bg-purple-600',
|
||||
'bg-red-500 dark:bg-red-600',
|
||||
'bg-indigo-500 dark:bg-indigo-600',
|
||||
'bg-pink-500 dark:bg-pink-600',
|
||||
'bg-gray-500 dark:bg-gray-600'
|
||||
];
|
||||
|
||||
// Calcular o período total do cronograma
|
||||
const todasAsDatas = cronograma.processos.flatMap(p => [p.data_inicio, p.data_fim]);
|
||||
const dataInicioTotal = new Date(Math.min(...todasAsDatas.map(d => new Date(d).getTime())));
|
||||
const dataFimTotal = new Date(Math.max(...todasAsDatas.map(d => new Date(d).getTime())));
|
||||
const duracaoTotal = differenceInDays(dataFimTotal, dataInicioTotal) + 1;
|
||||
|
||||
const calcularPosicaoELargura = (dataInicio: string, dataFim: string) => {
|
||||
const inicio = parseISO(dataInicio);
|
||||
const fim = parseISO(dataFim);
|
||||
|
||||
const diasDoInicio = differenceInDays(inicio, dataInicioTotal);
|
||||
const duracaoProcesso = differenceInDays(fim, inicio) + 1;
|
||||
|
||||
const left = (diasDoInicio / duracaoTotal) * 100;
|
||||
const width = (duracaoProcesso / duracaoTotal) * 100;
|
||||
|
||||
return { left: `${left}%`, width: `${width}%` };
|
||||
};
|
||||
|
||||
const calcularDiasCorridos = (dataInicio: string, dataFim: string) => {
|
||||
return differenceInDays(parseISO(dataFim), parseISO(dataInicio)) + 1;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-6xl max-h-[90vh] overflow-auto bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700">
|
||||
<CardHeader className="flex flex-row items-center justify-between border-b border-gray-200 dark:border-gray-700">
|
||||
<CardTitle className="flex items-center gap-2 text-gray-900 dark:text-gray-100">
|
||||
<Calendar className="w-5 h-5" />
|
||||
Cronograma da OF: {cronograma.ordem_fabricacao?.num_of} - Rev. {cronograma.revisao}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
{onExportPDF && (
|
||||
<Button onClick={onExportPDF} variant="outline" size="sm" className="border-gray-200 dark:border-gray-600">
|
||||
<FileDown className="w-4 h-4 mr-2" />
|
||||
Exportar PDF
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" onClick={onClose} className="hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-6 p-6">
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong>Gestor Responsável:</strong> {cronograma.gestor_profile?.full_name}
|
||||
</div>
|
||||
|
||||
{/* Tabela de Prazos */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Prazos de Produção da OF</h3>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse border border-gray-300 dark:border-gray-600">
|
||||
<thead className="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-gray-900 dark:text-gray-100">Processo</th>
|
||||
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-gray-900 dark:text-gray-100">Data de Início</th>
|
||||
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-gray-900 dark:text-gray-100">Data de Fim</th>
|
||||
<th className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-left text-gray-900 dark:text-gray-100">Dias Corridos</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{cronograma.processos
|
||||
.sort((a, b) => (a.ordem || 0) - (b.ordem || 0))
|
||||
.map((processo, index) => (
|
||||
<tr key={index} className={index % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-700'}>
|
||||
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 font-medium text-gray-900 dark:text-gray-100">
|
||||
{processo.nome_processo}
|
||||
</td>
|
||||
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-gray-900 dark:text-gray-100">
|
||||
{format(parseISO(processo.data_inicio), 'dd/MM/yyyy', { locale: ptBR })}
|
||||
</td>
|
||||
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-gray-900 dark:text-gray-100">
|
||||
{format(parseISO(processo.data_fim), 'dd/MM/yyyy', { locale: ptBR })}
|
||||
</td>
|
||||
<td className="border border-gray-300 dark:border-gray-600 px-4 py-2 text-center text-gray-900 dark:text-gray-100">
|
||||
{calcularDiasCorridos(processo.data_inicio, processo.data_fim)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Gráfico de Gantt */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Linha do Tempo dos Processos</h3>
|
||||
|
||||
{/* Cabeçalho com datas */}
|
||||
<div className="relative bg-gray-100 dark:bg-gray-700 h-8 rounded border border-gray-200 dark:border-gray-600">
|
||||
<div className="absolute left-0 top-1 text-xs font-medium px-2 text-gray-700 dark:text-gray-300">
|
||||
{format(dataInicioTotal, 'dd/MM/yyyy', { locale: ptBR })}
|
||||
</div>
|
||||
<div className="absolute right-0 top-1 text-xs font-medium px-2 text-gray-700 dark:text-gray-300">
|
||||
{format(dataFimTotal, 'dd/MM/yyyy', { locale: ptBR })}
|
||||
</div>
|
||||
<div className="absolute left-1/2 top-1 text-xs font-medium transform -translate-x-1/2 text-gray-700 dark:text-gray-300">
|
||||
{duracaoTotal} dias totais
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Barras dos processos */}
|
||||
<div className="space-y-3">
|
||||
{cronograma.processos
|
||||
.sort((a, b) => (a.ordem || 0) - (b.ordem || 0))
|
||||
.map((processo, index) => {
|
||||
const posicao = calcularPosicaoELargura(processo.data_inicio, processo.data_fim);
|
||||
const cor = cores[index % cores.length];
|
||||
const diasCorridos = calcularDiasCorridos(processo.data_inicio, processo.data_fim);
|
||||
const dataInicioFormatada = format(parseISO(processo.data_inicio), 'dd/MM', { locale: ptBR });
|
||||
const dataFimFormatada = format(parseISO(processo.data_fim), 'dd/MM', { locale: ptBR });
|
||||
|
||||
return (
|
||||
<div key={index} className="relative">
|
||||
<div className="flex items-center mb-1">
|
||||
<div className="w-32 text-sm font-medium truncate text-gray-900 dark:text-gray-100" title={processo.nome_processo}>
|
||||
{processo.nome_processo}
|
||||
</div>
|
||||
<div className="flex-1 relative h-8 bg-gray-200 dark:bg-gray-600 rounded ml-4">
|
||||
<div
|
||||
className={`absolute top-0 h-full ${cor} rounded flex items-center justify-between px-2 text-white text-xs font-medium`}
|
||||
style={posicao}
|
||||
title={`${processo.nome_processo}: ${dataInicioFormatada} - ${dataFimFormatada} (${diasCorridos} dias)`}
|
||||
>
|
||||
<span className="text-xs">{dataInicioFormatada}</span>
|
||||
<span className="text-xs font-bold">{diasCorridos}d</span>
|
||||
<span className="text-xs">{dataFimFormatada}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legenda */}
|
||||
<div className="flex flex-wrap gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">Legenda:</div>
|
||||
{cronograma.processos
|
||||
.sort((a, b) => (a.ordem || 0) - (b.ordem || 0))
|
||||
.map((processo, index) => {
|
||||
const cor = cores[index % cores.length];
|
||||
return (
|
||||
<div key={index} className="flex items-center gap-2 text-sm">
|
||||
<div className={`w-4 h-4 ${cor} rounded`}></div>
|
||||
<span className="text-gray-900 dark:text-gray-100">{processo.nome_processo}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,293 @@
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { CronogramaOf } from '@/hooks/useCronogramas';
|
||||
import { useBrandSettings } from '@/hooks/useBrandSettings';
|
||||
import jsPDF from 'jspdf';
|
||||
import { format, differenceInDays, parseISO } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
|
||||
interface CronogramaPDFProps {
|
||||
cronograma: CronogramaOf;
|
||||
onComplete?: () => void;
|
||||
}
|
||||
|
||||
export const CronogramaPDF: React.FC<CronogramaPDFProps> = ({ cronograma, onComplete }) => {
|
||||
const { brandSettings } = useBrandSettings();
|
||||
|
||||
const calcularDiasCorridos = (dataInicio: string, dataFim: string) => {
|
||||
return differenceInDays(parseISO(dataFim), parseISO(dataInicio)) + 1;
|
||||
};
|
||||
|
||||
const gerarPDF = async () => {
|
||||
const doc = new jsPDF('portrait', 'mm', 'a4');
|
||||
|
||||
// Configurações de cores (tons profissionais em cinza)
|
||||
const corCinzaClaro = [200, 200, 200]; // Cinza claro para cabeçalho
|
||||
const corCinzaMedio = [150, 150, 150]; // Cinza médio
|
||||
const corCinzaEscuro = [80, 80, 80]; // Cinza escuro para texto
|
||||
const corBranco = [255, 255, 255]; // Branco
|
||||
|
||||
const pageWidth = doc.internal.pageSize.width;
|
||||
const pageHeight = doc.internal.pageSize.height;
|
||||
const margin = 15;
|
||||
const usableWidth = pageWidth - (margin * 2);
|
||||
let yPosition = margin;
|
||||
|
||||
// CABEÇALHO COMPACTO
|
||||
// Fundo do cabeçalho - altura reduzida para 25px
|
||||
doc.setFillColor(corCinzaClaro[0], corCinzaClaro[1], corCinzaClaro[2]);
|
||||
doc.rect(0, 0, pageWidth, 25, 'F');
|
||||
|
||||
// Logo da empresa (tamanho reduzido)
|
||||
if (brandSettings.logo_url) {
|
||||
try {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
img.src = brandSettings.logo_url!;
|
||||
});
|
||||
|
||||
// Logo menor - 20x15
|
||||
const logoWidth = 20;
|
||||
const logoHeight = 15;
|
||||
doc.addImage(img, 'PNG', margin, 5, logoWidth, logoHeight);
|
||||
} catch (error) {
|
||||
console.log('Erro ao carregar logo:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Nome da empresa e título
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(brandSettings.company_name, brandSettings.logo_url ? margin + 25 : margin, 12);
|
||||
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('CRONOGRAMA DE PRODUÇÃO', brandSettings.logo_url ? margin + 25 : margin, 18);
|
||||
|
||||
yPosition = 35;
|
||||
|
||||
// TÍTULO DO CRONOGRAMA
|
||||
doc.setFillColor(240, 240, 240);
|
||||
doc.rect(margin, yPosition - 3, usableWidth, 12, 'F');
|
||||
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
doc.setFontSize(12);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
const titulo = `OF: ${cronograma.ordem_fabricacao?.num_of} - Revisão: ${cronograma.revisao}`;
|
||||
doc.text(titulo, margin + 3, yPosition + 3);
|
||||
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`Gestor: ${cronograma.gestor_profile?.full_name || 'N/A'}`, margin + 3, yPosition + 8);
|
||||
|
||||
yPosition += 18;
|
||||
|
||||
// RESUMO EXECUTIVO COMPACTO
|
||||
const todasAsDatas = cronograma.processos.flatMap(p => [p.data_inicio, p.data_fim]);
|
||||
const dataInicioTotal = new Date(Math.min(...todasAsDatas.map(d => new Date(d).getTime())));
|
||||
const dataFimTotal = new Date(Math.max(...todasAsDatas.map(d => new Date(d).getTime())));
|
||||
const duracaoTotal = differenceInDays(dataFimTotal, dataInicioTotal) + 1;
|
||||
|
||||
// Boxes do resumo em linha
|
||||
const boxWidth = (usableWidth) / 3;
|
||||
|
||||
doc.setFillColor(corBranco[0], corBranco[1], corBranco[2]);
|
||||
doc.setDrawColor(corCinzaMedio[0], corCinzaMedio[1], corCinzaMedio[2]);
|
||||
|
||||
// Box 1
|
||||
doc.rect(margin, yPosition, boxWidth - 2, 15, 'FD');
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(`${cronograma.processos.length} Processos`, margin + 3, yPosition + 6);
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('Total', margin + 3, yPosition + 11);
|
||||
|
||||
// Box 2
|
||||
doc.rect(margin + boxWidth, yPosition, boxWidth - 2, 15, 'FD');
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(`${duracaoTotal} dias`, margin + boxWidth + 3, yPosition + 6);
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('Duração', margin + boxWidth + 3, yPosition + 11);
|
||||
|
||||
// Box 3
|
||||
doc.rect(margin + boxWidth * 2, yPosition, boxWidth - 2, 15, 'FD');
|
||||
doc.setFontSize(8);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(`${format(dataInicioTotal, 'dd/MM', { locale: ptBR })} - ${format(dataFimTotal, 'dd/MM', { locale: ptBR })}`, margin + boxWidth * 2 + 3, yPosition + 6);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('Período', margin + boxWidth * 2 + 3, yPosition + 11);
|
||||
|
||||
yPosition += 22;
|
||||
|
||||
// TABELA COMPACTA
|
||||
doc.setFillColor(corCinzaClaro[0], corCinzaClaro[1], corCinzaClaro[2]);
|
||||
doc.rect(margin, yPosition, usableWidth, 8, 'F');
|
||||
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('CRONOGRAMA DETALHADO', margin + 3, yPosition + 5);
|
||||
|
||||
yPosition += 10;
|
||||
|
||||
// Cabeçalho da tabela - altura reduzida
|
||||
const colWidths = [usableWidth * 0.4, usableWidth * 0.2, usableWidth * 0.2, usableWidth * 0.2];
|
||||
const headers = ['Processo', 'Início', 'Fim', 'Duração'];
|
||||
|
||||
doc.setFillColor(245, 245, 245);
|
||||
doc.rect(margin, yPosition, usableWidth, 7, 'F');
|
||||
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
|
||||
let xPosition = margin;
|
||||
headers.forEach((header, index) => {
|
||||
doc.text(header, xPosition + 2, yPosition + 5);
|
||||
xPosition += colWidths[index];
|
||||
});
|
||||
|
||||
yPosition += 7;
|
||||
|
||||
// Linhas da tabela - altura reduzida em 40% (de 12 para 7.2)
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(8);
|
||||
|
||||
cronograma.processos
|
||||
.sort((a, b) => (a.ordem || 0) - (b.ordem || 0))
|
||||
.forEach((processo, index) => {
|
||||
// Alternância de cores
|
||||
if (index % 2 === 0) {
|
||||
doc.setFillColor(250, 250, 250);
|
||||
doc.rect(margin, yPosition, usableWidth, 7, 'F');
|
||||
}
|
||||
|
||||
xPosition = margin;
|
||||
const rowData = [
|
||||
processo.nome_processo.length > 25 ? processo.nome_processo.substring(0, 25) + '...' : processo.nome_processo,
|
||||
format(parseISO(processo.data_inicio), 'dd/MM', { locale: ptBR }),
|
||||
format(parseISO(processo.data_fim), 'dd/MM', { locale: ptBR }),
|
||||
`${calcularDiasCorridos(processo.data_inicio, processo.data_fim)}d`
|
||||
];
|
||||
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
rowData.forEach((data, colIndex) => {
|
||||
doc.text(data, xPosition + 2, yPosition + 5);
|
||||
xPosition += colWidths[colIndex];
|
||||
});
|
||||
|
||||
yPosition += 7;
|
||||
});
|
||||
|
||||
yPosition += 10;
|
||||
|
||||
// GRÁFICO DE GANTT VISUAL MELHORADO
|
||||
doc.setFillColor(corCinzaClaro[0], corCinzaClaro[1], corCinzaClaro[2]);
|
||||
doc.rect(margin, yPosition, usableWidth, 8, 'F');
|
||||
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('LINHA DO TEMPO VISUAL', margin + 3, yPosition + 5);
|
||||
|
||||
yPosition += 12;
|
||||
|
||||
// Escala de tempo
|
||||
doc.setTextColor(corCinzaMedio[0], corCinzaMedio[1], corCinzaMedio[2]);
|
||||
doc.setFontSize(8);
|
||||
doc.text(format(dataInicioTotal, 'dd/MM/yy', { locale: ptBR }), margin, yPosition - 2);
|
||||
doc.text(format(dataFimTotal, 'dd/MM/yy', { locale: ptBR }), margin + usableWidth - 20, yPosition - 2);
|
||||
doc.text(`${duracaoTotal} dias`, margin + usableWidth/2 - 10, yPosition - 2);
|
||||
|
||||
// Linha de base
|
||||
doc.setDrawColor(corCinzaMedio[0], corCinzaMedio[1], corCinzaMedio[2]);
|
||||
doc.line(margin, yPosition, margin + usableWidth, yPosition);
|
||||
|
||||
yPosition += 3;
|
||||
|
||||
// Barras dos processos - área reservada para nomes maior
|
||||
const cores = [
|
||||
[52, 152, 219], // Azul
|
||||
[46, 204, 113], // Verde
|
||||
[241, 196, 15], // Amarelo
|
||||
[155, 89, 182], // Roxo
|
||||
[231, 76, 60], // Vermelho
|
||||
[230, 126, 34], // Laranja
|
||||
[26, 188, 156], // Turquesa
|
||||
[127, 140, 141] // Cinza
|
||||
];
|
||||
|
||||
const nomeAreaWidth = 60; // Área reservada para nomes dos processos
|
||||
const graficoWidth = usableWidth - nomeAreaWidth - 5;
|
||||
|
||||
cronograma.processos
|
||||
.sort((a, b) => (a.ordem || 0) - (b.ordem || 0))
|
||||
.forEach((processo, index) => {
|
||||
const diasDoInicio = differenceInDays(parseISO(processo.data_inicio), dataInicioTotal);
|
||||
const duracaoProcesso = calcularDiasCorridos(processo.data_inicio, processo.data_fim);
|
||||
|
||||
const barraInicio = margin + nomeAreaWidth + (diasDoInicio / duracaoTotal) * graficoWidth;
|
||||
const barraLargura = Math.max(2, (duracaoProcesso / duracaoTotal) * graficoWidth);
|
||||
|
||||
// Nome do processo na área reservada
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
doc.setFontSize(7);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
const nomeProcesso = processo.nome_processo.length > 20 ?
|
||||
processo.nome_processo.substring(0, 20) + '...' :
|
||||
processo.nome_processo;
|
||||
doc.text(nomeProcesso, margin, yPosition + 2);
|
||||
|
||||
// Barra colorida
|
||||
const cor = cores[index % cores.length];
|
||||
doc.setFillColor(cor[0], cor[1], cor[2]);
|
||||
doc.rect(barraInicio, yPosition - 1, barraLargura, 6, 'F');
|
||||
|
||||
// Duração na barra (se houver espaço)
|
||||
if (barraLargura > 8) {
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFontSize(6);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text(`${duracaoProcesso}d`, barraInicio + barraLargura/2 - 2, yPosition + 2);
|
||||
}
|
||||
|
||||
yPosition += 8;
|
||||
});
|
||||
|
||||
// RODAPÉ COMPACTO
|
||||
doc.setFillColor(corCinzaClaro[0], corCinzaClaro[1], corCinzaClaro[2]);
|
||||
doc.rect(0, pageHeight - 15, pageWidth, 15, 'F');
|
||||
|
||||
doc.setTextColor(corCinzaEscuro[0], corCinzaEscuro[1], corCinzaEscuro[2]);
|
||||
doc.setFontSize(7);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`${brandSettings.company_name} - Sistema de Gestão`, margin, pageHeight - 8);
|
||||
doc.text(`${format(new Date(), 'dd/MM/yyyy HH:mm')}`, pageWidth - 35, pageHeight - 8);
|
||||
|
||||
// Download do PDF
|
||||
const nomeArquivo = `cronograma_${cronograma.ordem_fabricacao?.num_of}_rev${cronograma.revisao}_${format(new Date(), 'ddMMyyyy')}.pdf`;
|
||||
doc.save(nomeArquivo);
|
||||
|
||||
if (onComplete) {
|
||||
onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
gerarPDF();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [cronograma, onComplete, brandSettings]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,187 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Edit, Trash2, BarChart3, FileText } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { ptBR } from 'date-fns/locale';
|
||||
import { CronogramaOf } from '@/types/cronograma';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface CronogramaTableProps {
|
||||
cronogramas: CronogramaOf[];
|
||||
onEdit: (cronograma: CronogramaOf) => void;
|
||||
onDelete: (cronogramaId: string) => void;
|
||||
onViewChart: (cronograma: CronogramaOf) => void;
|
||||
onViewPDF: (cronograma: CronogramaOf) => void;
|
||||
canEdit?: boolean;
|
||||
canDelete?: boolean;
|
||||
}
|
||||
|
||||
export const CronogramaTable: React.FC<CronogramaTableProps> = ({
|
||||
cronogramas,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onViewChart,
|
||||
onViewPDF,
|
||||
canEdit = true,
|
||||
canDelete = true,
|
||||
}) => {
|
||||
const calcularDiasCorridos = (dataInicio: string, dataFim: string) => {
|
||||
const inicio = new Date(dataInicio);
|
||||
const fim = new Date(dataFim);
|
||||
const diffTime = Math.abs(fim.getTime() - inicio.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1;
|
||||
return diffDays;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="border-gray-200 dark:border-gray-700">
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">OF</TableHead>
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">Gestor</TableHead>
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">Revisão</TableHead>
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">Processos</TableHead>
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">Peso Total (kg)</TableHead>
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">Período Total</TableHead>
|
||||
<TableHead className="text-gray-900 dark:text-gray-100">Dias Corridos</TableHead>
|
||||
<TableHead className="text-right text-gray-900 dark:text-gray-100">Ações</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{cronogramas.map((cronograma) => {
|
||||
const primeiroProcesso = cronograma.processos[0];
|
||||
const ultimoProcesso = cronograma.processos[cronograma.processos.length - 1];
|
||||
const dataInicio = primeiroProcesso?.data_inicio;
|
||||
const dataFim = ultimoProcesso?.data_fim;
|
||||
const diasCorridos = dataInicio && dataFim ? calcularDiasCorridos(dataInicio, dataFim) : 0;
|
||||
|
||||
return (
|
||||
<TableRow key={cronograma.id} className="border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<TableCell className="font-medium">
|
||||
<div>
|
||||
<div className="font-semibold text-gray-900 dark:text-gray-100">{cronograma.ordem_fabricacao?.num_of}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{cronograma.ordem_fabricacao?.descritivo}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{cronograma.gestor_profile?.full_name || 'N/A'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="secondary" className="bg-gray-100 dark:bg-gray-700 text-gray-900 dark:text-gray-100">Rev. {cronograma.revisao}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-gray-900 dark:text-gray-100">
|
||||
{cronograma.processos.length} processo{cronograma.processos.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="text-sm text-gray-900 dark:text-gray-100 font-medium">
|
||||
{cronograma.peso_total ? `${Number(cronograma.peso_total).toLocaleString('pt-BR')} kg` : 'N/A'}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{dataInicio && dataFim ? (
|
||||
<div className="text-sm">
|
||||
<div className="text-gray-900 dark:text-gray-100">{format(new Date(dataInicio + 'T00:00:00'), 'dd/MM/yyyy', { locale: ptBR })}</div>
|
||||
<div className="text-gray-600 dark:text-gray-400">até</div>
|
||||
<div className="text-gray-900 dark:text-gray-100">{format(new Date(dataFim + 'T00:00:00'), 'dd/MM/yyyy', { locale: ptBR })}</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-500 dark:text-gray-400">-</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline" className="border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100">{diasCorridos} dias</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onViewChart(cronograma)}
|
||||
title="Ver gráfico Gantt"
|
||||
className="border-blue-200 dark:border-blue-600 bg-blue-50 dark:bg-blue-900/20 hover:bg-blue-100 dark:hover:bg-blue-800/30 text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
<BarChart3 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onViewPDF(cronograma)}
|
||||
title="Gerar PDF"
|
||||
className="border-green-200 dark:border-green-600 bg-green-50 dark:bg-green-900/20 hover:bg-green-100 dark:hover:bg-green-800/30 text-green-700 dark:text-green-300"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
</Button>
|
||||
{canEdit && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onEdit(cronograma)}
|
||||
title="Editar cronograma"
|
||||
className="border-gray-200 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
<Edit className="w-4 h-4 text-gray-700 dark:text-gray-300" />
|
||||
</Button>
|
||||
)}
|
||||
{canDelete && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
title="Remover cronograma"
|
||||
className="border-red-200 dark:border-red-600 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-800/30 text-red-700 dark:text-red-300"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent className="bg-white dark:bg-gray-800 border-gray-200 dark:border-gray-700">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-gray-900 dark:text-gray-100">Remover Cronograma</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-gray-600 dark:text-gray-400">
|
||||
Tem certeza que deseja remover o cronograma da OF {cronograma.ordem_fabricacao?.num_of}?
|
||||
Esta ação não pode ser desfeita.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="border-gray-200 dark:border-gray-600 text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700">Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => cronograma.id && onDelete(cronograma.id)}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
Remover
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user