Add supplies module shell
This commit is contained in:
@@ -11,6 +11,7 @@ const ProductGroupDetails = React.lazy(() => import('./pages/ProductGroupDetails
|
||||
const Replenishment = React.lazy(() => import('./pages/Replenishment'));
|
||||
const Cutting = React.lazy(() => import('./pages/Cutting'));
|
||||
const ProductionOrders = React.lazy(() => import('./pages/ProductionOrders'));
|
||||
const Supplies = React.lazy(() => import('./pages/Supplies'));
|
||||
const Clients = React.lazy(() => import('./pages/Clients'));
|
||||
const ClientDetails = React.lazy(() => import('./pages/ClientDetails'));
|
||||
const Campaigns = React.lazy(() => import('./pages/Campaigns'));
|
||||
@@ -53,6 +54,8 @@ function App() {
|
||||
<Route path="products/:id" element={<ProductDetails />} />
|
||||
<Route path="replenishment" element={<Replenishment />} />
|
||||
<Route path="cutting" element={<Cutting />} />
|
||||
<Route path="supplies" element={<Supplies />} />
|
||||
<Route path="supplies/:section" element={<Supplies />} />
|
||||
<Route path="stock" element={<Navigate to="/products" replace />} />
|
||||
<Route path="stock-alerts" element={<Navigate to="/products" replace />} />
|
||||
<Route path="production-orders" element={<ProductionOrders />} />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
||||
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield, Moon, Sun, ClipboardList, ShoppingCart, Scissors, Tags } from 'lucide-react';
|
||||
import { LayoutDashboard, Users, BarChart3, ChevronLeft, ChevronRight, Package, LogOut, Megaphone, Grid3X3, Shield, Moon, Sun, Tags, Boxes } from 'lucide-react';
|
||||
import type { DateRange, OrderData } from '../types';
|
||||
import { isSuperAdmin, logout } from '../dataService';
|
||||
import { rangeForLastDays } from '../dateRanges';
|
||||
@@ -74,9 +74,7 @@ const Layout = () => {
|
||||
items: [
|
||||
{ name: 'Produtos', href: '/products', icon: Package },
|
||||
{ name: 'Cadastros', href: '/registrations', icon: Tags },
|
||||
{ name: 'Reposição', href: '/replenishment', icon: ShoppingCart },
|
||||
{ name: 'Corte', href: '/cutting', icon: Scissors },
|
||||
{ name: 'Ordens de Produção', href: '/production-orders', icon: ClipboardList },
|
||||
{ name: 'Suprimentos', href: '/supplies', icon: Boxes },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
338
src/pages/Supplies.tsx
Normal file
338
src/pages/Supplies.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
import { useState } from 'react';
|
||||
import { Link as RouterLink, Navigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
BarChart3,
|
||||
Boxes,
|
||||
ClipboardCheck,
|
||||
ClipboardList,
|
||||
Download,
|
||||
Link as LinkIcon,
|
||||
Package,
|
||||
RefreshCw,
|
||||
Repeat2,
|
||||
Ruler,
|
||||
Search,
|
||||
Scissors,
|
||||
ShoppingCart,
|
||||
Truck,
|
||||
Warehouse,
|
||||
} from 'lucide-react';
|
||||
|
||||
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
||||
|
||||
const pageClassName = 'mx-auto flex w-full max-w-7xl flex-col gap-6';
|
||||
const panelClassName = 'rounded-2xl border border-dark-border bg-dark-card shadow-sm';
|
||||
const mutedPanelClassName = 'rounded-2xl border border-dark-border bg-dark-card/70 shadow-sm';
|
||||
const buttonClassName = 'inline-flex h-10 items-center justify-center gap-2 rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer';
|
||||
const inputClassName = 'h-10 w-full rounded-lg border border-dark-border bg-dark-input px-3 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary';
|
||||
const emptyStateClassName = 'flex min-h-[190px] flex-col items-center justify-center gap-2 px-4 py-10 text-center';
|
||||
|
||||
const stats = [
|
||||
{ label: 'Total em estoque', value: '0 kg' },
|
||||
{ label: 'Lotes ativos', value: '0' },
|
||||
{ label: 'Rolos', value: '0' },
|
||||
{ label: 'Alertas', value: '0' },
|
||||
];
|
||||
|
||||
const inventoryTabs: Array<{ id: InventoryTab; name: string; icon: typeof BarChart3 }> = [
|
||||
{ id: 'dashboard', name: 'Dashboard', icon: Warehouse },
|
||||
{ id: 'balance', name: 'Saldo', icon: BarChart3 },
|
||||
{ id: 'receipts', name: 'Recebimentos', icon: Truck },
|
||||
{ id: 'inventory', name: 'Inventário', icon: ClipboardCheck },
|
||||
{ id: 'movements', name: 'Movimentações', icon: Repeat2 },
|
||||
];
|
||||
|
||||
const receiptCategories = [
|
||||
{ name: 'Malha / Tecido', icon: Ruler },
|
||||
{ name: 'Embalagem', icon: Package },
|
||||
{ name: 'Material de Limpeza', icon: Boxes },
|
||||
{ name: 'Acessórios / Botões', icon: Warehouse },
|
||||
{ name: 'Etiquetas', icon: LinkIcon },
|
||||
{ name: 'Outro material', icon: ClipboardList },
|
||||
];
|
||||
|
||||
const Header = ({ title, subtitle, backTo }: { title: string; subtitle: string; backTo?: string }) => (
|
||||
<div className="flex flex-col gap-3">
|
||||
{backTo && (
|
||||
<RouterLink to={backTo} className="inline-flex w-fit items-center gap-2 text-sm font-bold text-dark-muted transition-colors hover:text-dark-text">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Suprimentos
|
||||
</RouterLink>
|
||||
)}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-dark-text">{title}</h1>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">{subtitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ModuleCard = ({
|
||||
title,
|
||||
description,
|
||||
icon: Icon,
|
||||
to,
|
||||
disabled,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: typeof Package;
|
||||
to?: string;
|
||||
disabled?: boolean;
|
||||
}) => {
|
||||
const content = (
|
||||
<>
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-xl border border-dark-border bg-dark-input text-brand-primary">
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<h2 className="text-base font-bold text-dark-text">{title}</h2>
|
||||
{disabled ? (
|
||||
<span className="rounded-full bg-dark-input px-2 py-1 text-[11px] font-bold uppercase tracking-wide text-dark-muted">Em breve</span>
|
||||
) : (
|
||||
<ArrowRight className="mt-0.5 h-4 w-4 shrink-0 text-dark-muted" />
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-2 text-sm font-semibold text-dark-muted">{description}</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
if (disabled || !to) {
|
||||
return (
|
||||
<div className={`${panelClassName} flex min-h-32 items-start gap-4 p-5 opacity-70`}>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RouterLink to={to} className={`${panelClassName} flex min-h-32 items-start gap-4 p-5 transition-colors hover:border-brand-primary`}>
|
||||
{content}
|
||||
</RouterLink>
|
||||
);
|
||||
};
|
||||
|
||||
const SuppliesHub = () => (
|
||||
<div className={pageClassName}>
|
||||
<Header title="Suprimentos" subtitle="Corte, estoque, malha e compras em um fluxo operacional." />
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<ModuleCard title="Planejamento de Corte" description="Ruptura por cor e tamanho, montagem do corte e prioridade por cobertura." icon={Scissors} to="/cutting" />
|
||||
<ModuleCard title="Controle de Estoque" description="Saldo, lotes, recebimentos, inventário e movimentações." icon={Package} to="/supplies/inventory" />
|
||||
<ModuleCard title="Planos / Ordem de Produção" description="Ordens geradas pelo corte e acompanhamento de produção." icon={ClipboardList} to="/production-orders" />
|
||||
<ModuleCard title="Planejamento de Malha" description="Fila de compra/recebimento de matéria-prima para alimentar o corte." icon={Ruler} to="/supplies/fabric-planning" />
|
||||
<ModuleCard title="Ordens de Compra" description="Compra, recebimento e vínculo financeiro de materiais." icon={ShoppingCart} disabled />
|
||||
<ModuleCard title="Necessidade de Compra" description="Itens abaixo do mínimo e necessidade projetada para compra." icon={BarChart3} to="/replenishment" />
|
||||
<ModuleCard title="Relatórios" description="Extratos operacionais de estoque, consumo e movimentações." icon={Download} disabled />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const StatGrid = () => (
|
||||
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
|
||||
{stats.map(stat => (
|
||||
<div key={stat.label} className="rounded-xl border border-dark-border bg-dark-input/40 px-4 py-4 text-center">
|
||||
<p className="text-2xl font-bold text-dark-text">{stat.value}</p>
|
||||
<p className="mt-1 text-xs font-semibold text-dark-muted">{stat.label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const InventoryDashboard = () => (
|
||||
<div className="space-y-4">
|
||||
<StatGrid />
|
||||
<div>
|
||||
<h2 className="mb-3 text-base font-bold text-dark-text">Por categoria de material</h2>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
{[
|
||||
{ title: 'Malha / Tecido', icon: Ruler },
|
||||
{ title: 'Botões / Acessórios', icon: Warehouse },
|
||||
].map(item => (
|
||||
<div key={item.title} className={`${mutedPanelClassName} p-5`}>
|
||||
<item.icon className="h-6 w-6 text-brand-primary" />
|
||||
<h3 className="mt-4 text-sm font-bold text-dark-text">{item.title}</h3>
|
||||
<p className="mt-1 text-sm font-bold text-dark-muted">Em breve</p>
|
||||
<p className="text-xs font-semibold text-dark-muted">Aguardando cadastro</p>
|
||||
<span className="mt-3 inline-flex rounded-full bg-dark-input px-2 py-1 text-[11px] font-bold text-dark-muted">Não configurado</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<h2 className="flex items-center gap-2 text-base font-bold text-dark-text"><AlertTriangle className="h-4 w-4 text-yellow-400" /> Alertas de estoque</h2>
|
||||
<p className="mt-4 text-sm font-semibold text-dark-muted">Nenhum alerta no momento.</p>
|
||||
</div>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-bold text-dark-text">Últimas entradas</h2>
|
||||
<button type="button" className={buttonClassName}>Nova entrada</button>
|
||||
</div>
|
||||
<p className="mt-5 text-sm font-semibold text-dark-muted">Nenhuma entrada registrada.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const BalanceTab = () => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button type="button" className={buttonClassName}><Download className="h-4 w-4" /> Exportar saldo CSV</button>
|
||||
<button type="button" className={buttonClassName}><LinkIcon className="h-4 w-4" /> Sincronizar com Tiny</button>
|
||||
</div>
|
||||
<div className={`${panelClassName} p-4`}>
|
||||
<div className="grid grid-cols-1 gap-3 lg:grid-cols-[1fr_220px_220px]">
|
||||
<label className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
|
||||
<input className={`${inputClassName} pl-9`} placeholder="Buscar por SKU, lote, tipo ou fornecedor..." />
|
||||
</label>
|
||||
<select className={inputClassName}><option>Todos os tipos</option></select>
|
||||
<select className={inputClassName}><option>Todos os fornecedores</option></select>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<StatGrid />
|
||||
</div>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="text-base font-bold text-dark-text">Saldo por tipo</h2>
|
||||
<span className="text-xs font-semibold text-dark-muted">Clique para expandir lotes</span>
|
||||
</div>
|
||||
<div className={emptyStateClassName}>
|
||||
<Package className="h-8 w-8 text-brand-primary" />
|
||||
<h3 className="text-base font-bold text-dark-text">Nenhum item em estoque</h3>
|
||||
<p className="text-sm font-semibold text-dark-muted">Registre entradas para ver o saldo aqui.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const ReceiptsTab = () => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['Novo recebimento', 'Pendentes', 'Histórico'].map((item, index) => (
|
||||
<button key={item} type="button" className={`${buttonClassName} ${index === 0 ? 'border-brand-primary bg-brand-primary text-brand-contrast' : ''}`}>
|
||||
{item}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<h2 className="text-base font-bold text-dark-text">Registrar recebimento</h2>
|
||||
<div className="mt-4 rounded-xl border border-dark-border bg-dark-input px-4 py-3 text-sm font-semibold text-dark-muted">
|
||||
Preencha o que chegou. O financeiro vincula OC/NF e aprova o lançamento.
|
||||
</div>
|
||||
<p className="mt-5 text-xs font-bold uppercase tracking-widest text-dark-muted">1. Qual categoria de produto chegou?</p>
|
||||
<div className="mt-3 grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
{receiptCategories.map(category => (
|
||||
<button key={category.name} type="button" className="flex min-h-24 flex-col items-center justify-center gap-3 rounded-xl border border-dark-border bg-dark-input/40 p-4 text-center font-bold text-dark-text transition-colors hover:border-brand-primary cursor-pointer">
|
||||
<category.icon className="h-6 w-6 text-brand-primary" />
|
||||
{category.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const InventoryCountTab = () => (
|
||||
<div className="space-y-4">
|
||||
<button type="button" className={buttonClassName}><Download className="h-4 w-4" /> Exportar inventário CSV</button>
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<h2 className="text-base font-bold text-dark-text">Inventário físico</h2>
|
||||
<div className="mt-4 rounded-xl border border-dark-border bg-dark-input px-4 py-3 text-sm font-semibold text-dark-muted">
|
||||
Compare o estoque do sistema com a contagem física. O ajuste gera movimentação com justificativa.
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-1 gap-3 md:grid-cols-[1fr_auto]">
|
||||
<input className={inputClassName} placeholder="Buscar item..." />
|
||||
<button type="button" className={buttonClassName}><RefreshCw className="h-4 w-4" /> Recarregar</button>
|
||||
</div>
|
||||
<div className={emptyStateClassName}>
|
||||
<ClipboardCheck className="h-8 w-8 text-brand-primary" />
|
||||
<h3 className="text-base font-bold text-dark-text">Nenhum lote para inventariar</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const MovementsTab = () => (
|
||||
<div className="space-y-4">
|
||||
<button type="button" className={buttonClassName}><Download className="h-4 w-4" /> Exportar movimentações CSV</button>
|
||||
<div className={`${panelClassName} p-4`}>
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-[1fr_220px]">
|
||||
<label className="relative">
|
||||
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
|
||||
<input className={`${inputClassName} pl-9`} placeholder="Buscar lote, tipo ou fornecedor..." />
|
||||
</label>
|
||||
<select className={inputClassName}><option>Todos os tipos</option></select>
|
||||
</div>
|
||||
<div className={emptyStateClassName}>
|
||||
<Repeat2 className="h-8 w-8 text-brand-primary" />
|
||||
<h3 className="text-base font-bold text-dark-text">Nenhuma movimentação ainda</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const InventoryScreen = () => {
|
||||
const [activeTab, setActiveTab] = useState<InventoryTab>('dashboard');
|
||||
|
||||
return (
|
||||
<div className={pageClassName}>
|
||||
<Header title="Controle de Estoque" subtitle="Saldo, lotes, entradas e inventário." backTo="/supplies" />
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{inventoryTabs.map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`inline-flex h-10 items-center justify-center gap-2 rounded-lg px-3 text-sm font-bold transition-colors cursor-pointer ${
|
||||
activeTab === tab.id ? 'bg-brand-primary text-brand-contrast' : 'bg-dark-input text-dark-text hover:bg-dark-card'
|
||||
}`}
|
||||
>
|
||||
<tab.icon className="h-4 w-4" />
|
||||
{tab.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{activeTab === 'dashboard' && <InventoryDashboard />}
|
||||
{activeTab === 'balance' && <BalanceTab />}
|
||||
{activeTab === 'receipts' && <ReceiptsTab />}
|
||||
{activeTab === 'inventory' && <InventoryCountTab />}
|
||||
{activeTab === 'movements' && <MovementsTab />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FabricPlanningScreen = () => (
|
||||
<div className={pageClassName}>
|
||||
<Header title="Planejamento de Malha" subtitle="Fila de matéria-prima para compra, recebimento e abastecimento do corte." backTo="/supplies" />
|
||||
<div className={`${panelClassName} p-5`}>
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-bold text-dark-text">Planos de malha</h2>
|
||||
<p className="mt-1 text-sm font-semibold text-dark-muted">Entrada operacional preparada para a próxima etapa do módulo.</p>
|
||||
</div>
|
||||
<span className="w-fit rounded-full bg-dark-input px-3 py-1 text-xs font-bold uppercase tracking-wide text-dark-muted">Em preparação</span>
|
||||
</div>
|
||||
<div className={emptyStateClassName}>
|
||||
<Ruler className="h-8 w-8 text-brand-primary" />
|
||||
<h3 className="text-base font-bold text-dark-text">Nenhum plano de malha cadastrado</h3>
|
||||
<p className="text-sm font-semibold text-dark-muted">Quando houver planos, eles alimentarão recebimentos e corte.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const Supplies = () => {
|
||||
const { section } = useParams<{ section?: string }>();
|
||||
|
||||
if (!section) return <SuppliesHub />;
|
||||
if (section === 'inventory') return <InventoryScreen />;
|
||||
if (section === 'fabric-planning') return <FabricPlanningScreen />;
|
||||
|
||||
return <Navigate to="/supplies" replace />;
|
||||
};
|
||||
|
||||
export default Supplies;
|
||||
Reference in New Issue
Block a user