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