Compare commits
7 Commits
c7808702fa
...
c68d3c7ed4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c68d3c7ed4 | ||
|
|
2b1bb8eda9 | ||
|
|
4d659d5eb6 | ||
|
|
fcb7eaf51d | ||
|
|
be1924bc94 | ||
|
|
3928717293 | ||
|
|
3bea89a8ac |
@@ -11,6 +11,7 @@ const ProductGroupDetails = React.lazy(() => import('./pages/ProductGroupDetails
|
|||||||
const Replenishment = React.lazy(() => import('./pages/Replenishment'));
|
const Replenishment = React.lazy(() => import('./pages/Replenishment'));
|
||||||
const Cutting = React.lazy(() => import('./pages/Cutting'));
|
const Cutting = React.lazy(() => import('./pages/Cutting'));
|
||||||
const ProductionOrders = React.lazy(() => import('./pages/ProductionOrders'));
|
const ProductionOrders = React.lazy(() => import('./pages/ProductionOrders'));
|
||||||
|
const Supplies = React.lazy(() => import('./pages/Supplies'));
|
||||||
const Clients = React.lazy(() => import('./pages/Clients'));
|
const Clients = React.lazy(() => import('./pages/Clients'));
|
||||||
const ClientDetails = React.lazy(() => import('./pages/ClientDetails'));
|
const ClientDetails = React.lazy(() => import('./pages/ClientDetails'));
|
||||||
const Campaigns = React.lazy(() => import('./pages/Campaigns'));
|
const Campaigns = React.lazy(() => import('./pages/Campaigns'));
|
||||||
@@ -53,6 +54,8 @@ function App() {
|
|||||||
<Route path="products/:id" element={<ProductDetails />} />
|
<Route path="products/:id" element={<ProductDetails />} />
|
||||||
<Route path="replenishment" element={<Replenishment />} />
|
<Route path="replenishment" element={<Replenishment />} />
|
||||||
<Route path="cutting" element={<Cutting />} />
|
<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" element={<Navigate to="/products" replace />} />
|
||||||
<Route path="stock-alerts" element={<Navigate to="/products" replace />} />
|
<Route path="stock-alerts" element={<Navigate to="/products" replace />} />
|
||||||
<Route path="production-orders" element={<ProductionOrders />} />
|
<Route path="production-orders" element={<ProductionOrders />} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Outlet, Link, useLocation } from 'react-router-dom';
|
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 type { DateRange, OrderData } from '../types';
|
||||||
import { isSuperAdmin, logout } from '../dataService';
|
import { isSuperAdmin, logout } from '../dataService';
|
||||||
import { rangeForLastDays } from '../dateRanges';
|
import { rangeForLastDays } from '../dateRanges';
|
||||||
@@ -74,9 +74,7 @@ const Layout = () => {
|
|||||||
items: [
|
items: [
|
||||||
{ name: 'Produtos', href: '/products', icon: Package },
|
{ name: 'Produtos', href: '/products', icon: Package },
|
||||||
{ name: 'Cadastros', href: '/registrations', icon: Tags },
|
{ name: 'Cadastros', href: '/registrations', icon: Tags },
|
||||||
{ name: 'Reposição', href: '/replenishment', icon: ShoppingCart },
|
{ name: 'Suprimentos', href: '/supplies', icon: Boxes },
|
||||||
{ name: 'Corte', href: '/cutting', icon: Scissors },
|
|
||||||
{ name: 'Ordens de Produção', href: '/production-orders', icon: ClipboardList },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -118,7 +116,12 @@ const Layout = () => {
|
|||||||
|
|
||||||
<nav className="flex-1 space-y-5 overflow-y-auto px-3 py-4">
|
<nav className="flex-1 space-y-5 overflow-y-auto px-3 py-4">
|
||||||
{navigationSections.map((section) => (
|
{navigationSections.map((section) => (
|
||||||
<div key={section.label} className="space-y-1.5">
|
<div
|
||||||
|
key={section.label}
|
||||||
|
className={`space-y-1.5 ${
|
||||||
|
isSidebarCollapsed ? 'border-t border-dark-border pt-5 first:border-t-0 first:pt-0' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{!isSidebarCollapsed && (
|
{!isSidebarCollapsed && (
|
||||||
<div className="px-3 pb-1 text-[11px] font-bold uppercase tracking-widest text-dark-muted">
|
<div className="px-3 pb-1 text-[11px] font-bold uppercase tracking-widest text-dark-muted">
|
||||||
{section.label}
|
{section.label}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useOutletContext } from 'react-router-dom';
|
import { Link, useOutletContext } from 'react-router-dom';
|
||||||
import { AlertTriangle, ClipboardList, Download, Layers3, Package, Palette, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2, X } from 'lucide-react';
|
import { AlertTriangle, ArrowLeft, ClipboardList, Download, Layers3, Package, Palette, RotateCcw, Ruler, Save as SaveIcon, Scissors, Search, Settings2, X } from 'lucide-react';
|
||||||
import DateRangePicker from '../components/DateRangePicker';
|
import DateRangePicker from '../components/DateRangePicker';
|
||||||
import PaginationControls from '../components/PaginationControls';
|
import PaginationControls from '../components/PaginationControls';
|
||||||
import ProductColorBadge from '../components/ProductColorBadge';
|
import ProductColorBadge from '../components/ProductColorBadge';
|
||||||
@@ -399,6 +399,10 @@ const Cutting = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid grid-cols-1 gap-4 2xl:grid-cols-[minmax(520px,1fr)_auto] 2xl:items-start">
|
<div className="grid grid-cols-1 gap-4 2xl:grid-cols-[minmax(520px,1fr)_auto] 2xl:items-start">
|
||||||
<div>
|
<div>
|
||||||
|
<Link to="/supplies" className="mb-3 inline-flex items-center gap-2 text-sm font-bold text-zinc-500 transition-colors hover:text-zinc-900 dark:text-dark-muted dark:hover:text-dark-text">
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Suprimentos
|
||||||
|
</Link>
|
||||||
<h1 className="mb-2 text-2xl font-bold text-zinc-900 dark:text-dark-text">Plano de Corte</h1>
|
<h1 className="mb-2 text-2xl font-bold text-zinc-900 dark:text-dark-text">Plano de Corte</h1>
|
||||||
<p className="font-medium text-zinc-500 dark:text-dark-muted">
|
<p className="font-medium text-zinc-500 dark:text-dark-muted">
|
||||||
Necessidade por família, cor e tamanho calculada com vendas, estoque e cobertura alvo.
|
Necessidade por família, cor e tamanho calculada com vendas, estoque e cobertura alvo.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useOutletContext } from 'react-router-dom';
|
import { Link, useOutletContext } from 'react-router-dom';
|
||||||
import { CalendarDays, CheckCircle2, ClipboardList, Clock3, Download, PackageCheck, Search } from 'lucide-react';
|
import { ArrowLeft, CalendarDays, CheckCircle2, ClipboardList, Clock3, Download, PackageCheck, Search } from 'lucide-react';
|
||||||
import DateRangePicker from '../components/DateRangePicker';
|
import DateRangePicker from '../components/DateRangePicker';
|
||||||
import PaginationControls from '../components/PaginationControls';
|
import PaginationControls from '../components/PaginationControls';
|
||||||
import RefreshStatus from '../components/RefreshStatus';
|
import RefreshStatus from '../components/RefreshStatus';
|
||||||
@@ -198,6 +198,10 @@ const ProductionOrders = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
<Link to="/supplies" className="mb-3 inline-flex items-center gap-2 text-sm font-bold text-dark-muted transition-colors hover:text-dark-text">
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Suprimentos
|
||||||
|
</Link>
|
||||||
<h1 className="text-2xl font-bold text-dark-text">Ordens de Produção</h1>
|
<h1 className="text-2xl font-bold text-dark-text">Ordens de Produção</h1>
|
||||||
<p className="mt-2 text-dark-muted font-medium">Acompanhe as ordens de produção sincronizadas do Tiny.</p>
|
<p className="mt-2 text-dark-muted font-medium">Acompanhe as ordens de produção sincronizadas do Tiny.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
|
import { Link, useOutletContext, useSearchParams } from 'react-router-dom';
|
||||||
import { AlertTriangle, CheckCircle2, Download, Package, Search, TrendingUp } from 'lucide-react';
|
import { AlertTriangle, ArrowLeft, CheckCircle2, Download, Package, Search, TrendingUp } from 'lucide-react';
|
||||||
import DateRangePicker from '../components/DateRangePicker';
|
import DateRangePicker from '../components/DateRangePicker';
|
||||||
import PaginationControls from '../components/PaginationControls';
|
import PaginationControls from '../components/PaginationControls';
|
||||||
import RefreshStatus from '../components/RefreshStatus';
|
import RefreshStatus from '../components/RefreshStatus';
|
||||||
@@ -314,6 +314,10 @@ const Replenishment = () => {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="grid grid-cols-1 gap-4 2xl:grid-cols-[minmax(520px,1fr)_auto] 2xl:items-start">
|
<div className="grid grid-cols-1 gap-4 2xl:grid-cols-[minmax(520px,1fr)_auto] 2xl:items-start">
|
||||||
<div>
|
<div>
|
||||||
|
<Link to="/supplies" className="mb-3 inline-flex items-center gap-2 text-sm font-bold text-zinc-500 transition-colors hover:text-zinc-900 dark:text-dark-muted dark:hover:text-dark-text">
|
||||||
|
<ArrowLeft className="h-4 w-4" />
|
||||||
|
Suprimentos
|
||||||
|
</Link>
|
||||||
<h1 className="text-2xl font-bold mb-2 text-zinc-900 dark:text-dark-text">Necessidade de Reposição</h1>
|
<h1 className="text-2xl font-bold mb-2 text-zinc-900 dark:text-dark-text">Necessidade de Reposição</h1>
|
||||||
<p className="text-zinc-500 dark:text-dark-muted font-medium">
|
<p className="text-zinc-500 dark:text-dark-muted font-medium">
|
||||||
O período selecionado define a base de vendas; a cobertura define quantos dias de estoque sugerir.
|
O período selecionado define a base de vendas; a cobertura define quantos dias de estoque sugerir.
|
||||||
|
|||||||
682
src/pages/Supplies.tsx
Normal file
682
src/pages/Supplies.tsx
Normal file
@@ -0,0 +1,682 @@
|
|||||||
|
import { type FormEvent, 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,
|
||||||
|
Save,
|
||||||
|
Search,
|
||||||
|
Scissors,
|
||||||
|
Truck,
|
||||||
|
Warehouse,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
type InventoryTab = 'dashboard' | 'balance' | 'receipts' | 'inventory' | 'movements';
|
||||||
|
type ReceiptView = 'new' | 'pending' | 'history';
|
||||||
|
type FabricPlan = {
|
||||||
|
id: string;
|
||||||
|
material: string;
|
||||||
|
color: string;
|
||||||
|
quantityKg: number;
|
||||||
|
supplier: string;
|
||||||
|
priority: string;
|
||||||
|
};
|
||||||
|
type SupplyReceipt = {
|
||||||
|
id: string;
|
||||||
|
category: string;
|
||||||
|
product: string;
|
||||||
|
quantity: number;
|
||||||
|
unit: string;
|
||||||
|
supplier: string;
|
||||||
|
invoice: string;
|
||||||
|
notes: string;
|
||||||
|
status: 'pending' | 'approved';
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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 formatNumber = (value: number, maximumFractionDigits = 2) => (
|
||||||
|
new Intl.NumberFormat('pt-BR', { maximumFractionDigits }).format(value)
|
||||||
|
);
|
||||||
|
|
||||||
|
const parseDecimal = (value: string) => {
|
||||||
|
const number = Number(value.replace(',', '.'));
|
||||||
|
return Number.isFinite(number) ? number : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadFabricPlans = (): FabricPlan[] => {
|
||||||
|
try {
|
||||||
|
const rawPlans = localStorage.getItem('nexstar_fabric_plans');
|
||||||
|
if (!rawPlans) return [];
|
||||||
|
const parsed = JSON.parse(rawPlans);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadReceipts = (): SupplyReceipt[] => {
|
||||||
|
try {
|
||||||
|
const rawReceipts = localStorage.getItem('nexstar_supply_receipts');
|
||||||
|
if (!rawReceipts) return [];
|
||||||
|
const parsed = JSON.parse(rawReceipts);
|
||||||
|
return Array.isArray(parsed) ? parsed : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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: Array<{ name: string; icon: typeof Package }> = [
|
||||||
|
{ 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,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: typeof Package;
|
||||||
|
to: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<RouterLink to={to} className={`${panelClassName} flex min-h-32 items-start gap-4 p-5 transition-colors hover:border-brand-primary`}>
|
||||||
|
<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>
|
||||||
|
<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>
|
||||||
|
</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="Ordens 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 matéria-prima para compra, recebimento e abastecimento do corte." icon={Ruler} to="/supplies/fabric-planning" />
|
||||||
|
<ModuleCard title="Necessidade de Compra" description="Itens abaixo do mínimo e necessidade projetada para compra." icon={BarChart3} to="/supplies/purchase-needs" />
|
||||||
|
</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 className={`${panelClassName} p-5`}>
|
||||||
|
<h2 className="text-base font-bold text-dark-text">Por categoria de material</h2>
|
||||||
|
<div className={emptyStateClassName}>
|
||||||
|
<Warehouse className="h-8 w-8 text-brand-primary" />
|
||||||
|
<h3 className="text-base font-bold text-dark-text">Nenhuma categoria com saldo</h3>
|
||||||
|
<p className="text-sm font-semibold text-dark-muted">Registre entradas para agrupar o estoque por tipo de material.</p>
|
||||||
|
</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 ReceiptList = ({
|
||||||
|
receipts,
|
||||||
|
emptyTitle,
|
||||||
|
onApprove,
|
||||||
|
onRemove,
|
||||||
|
}: {
|
||||||
|
receipts: SupplyReceipt[];
|
||||||
|
emptyTitle: string;
|
||||||
|
onApprove: (receiptId: string) => void;
|
||||||
|
onRemove: (receiptId: string) => void;
|
||||||
|
}) => (
|
||||||
|
<div className={`${panelClassName} overflow-hidden`}>
|
||||||
|
{receipts.length ? (
|
||||||
|
<div className="divide-y divide-dark-border">
|
||||||
|
{receipts.map(receipt => (
|
||||||
|
<div key={receipt.id} className="grid grid-cols-1 gap-3 p-4 lg:grid-cols-[1.2fr_120px_1fr_100px_auto] lg:items-center">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-dark-text">{receipt.product}</p>
|
||||||
|
<p className="text-xs font-semibold text-dark-muted">{receipt.category} · NF {receipt.invoice}</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-bold text-dark-text">{formatNumber(receipt.quantity)} {receipt.unit}</p>
|
||||||
|
<p className="text-sm font-semibold text-dark-muted">{receipt.supplier}</p>
|
||||||
|
<span className={`w-fit rounded-full border px-2.5 py-1 text-xs font-bold ${
|
||||||
|
receipt.status === 'approved'
|
||||||
|
? 'border-emerald-400/30 bg-emerald-400/10 text-emerald-300'
|
||||||
|
: 'border-amber-400/30 bg-amber-400/10 text-amber-300'
|
||||||
|
}`}>
|
||||||
|
{receipt.status === 'approved' ? 'Aprovado' : 'Pendente'}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-wrap gap-2 lg:justify-end">
|
||||||
|
{receipt.status === 'pending' && (
|
||||||
|
<button type="button" onClick={() => onApprove(receipt.id)} className={buttonClassName}>
|
||||||
|
Aprovar
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button type="button" onClick={() => onRemove(receipt.id)} className="inline-flex h-10 items-center justify-center rounded-lg px-3 text-sm font-bold text-red-400 transition-colors hover:bg-red-500/10 cursor-pointer">
|
||||||
|
Remover
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={emptyStateClassName}>
|
||||||
|
<Truck className="h-8 w-8 text-brand-primary" />
|
||||||
|
<h3 className="text-base font-bold text-dark-text">{emptyTitle}</h3>
|
||||||
|
<p className="text-sm font-semibold text-dark-muted">Registre um recebimento para preencher esta lista.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ReceiptsTab = () => {
|
||||||
|
const [activeView, setActiveView] = useState<ReceiptView>('new');
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState(receiptCategories[0].name);
|
||||||
|
const [receipts, setReceipts] = useState<SupplyReceipt[]>(loadReceipts);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
product: '',
|
||||||
|
quantity: '',
|
||||||
|
unit: 'kg',
|
||||||
|
supplier: '',
|
||||||
|
invoice: '',
|
||||||
|
notes: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pendingReceipts = receipts.filter(receipt => receipt.status === 'pending');
|
||||||
|
const visibleReceipts = activeView === 'pending' ? pendingReceipts : receipts;
|
||||||
|
|
||||||
|
const saveReceipts = (nextReceipts: SupplyReceipt[]) => {
|
||||||
|
setReceipts(nextReceipts);
|
||||||
|
localStorage.setItem('nexstar_supply_receipts', JSON.stringify(nextReceipts));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReceiptSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const product = form.product.trim();
|
||||||
|
const quantity = parseDecimal(form.quantity);
|
||||||
|
if (!product || quantity <= 0) return;
|
||||||
|
|
||||||
|
const nextReceipt: SupplyReceipt = {
|
||||||
|
id: `${Date.now()}`,
|
||||||
|
category: selectedCategory,
|
||||||
|
product,
|
||||||
|
quantity,
|
||||||
|
unit: form.unit,
|
||||||
|
supplier: form.supplier.trim() || 'Sem fornecedor',
|
||||||
|
invoice: form.invoice.trim() || '-',
|
||||||
|
notes: form.notes.trim(),
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
saveReceipts([nextReceipt, ...receipts]);
|
||||||
|
setForm({ product: '', quantity: '', unit: 'kg', supplier: '', invoice: '', notes: '' });
|
||||||
|
setActiveView('pending');
|
||||||
|
};
|
||||||
|
|
||||||
|
const markApproved = (receiptId: string) => {
|
||||||
|
saveReceipts(receipts.map(receipt => (
|
||||||
|
receipt.id === receiptId ? { ...receipt, status: 'approved' } : receipt
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{ id: 'new', label: 'Novo recebimento' },
|
||||||
|
{ id: 'pending', label: `Pendentes (${pendingReceipts.length})` },
|
||||||
|
{ id: 'history', label: `Histórico (${receipts.length})` },
|
||||||
|
].map(item => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveView(item.id as ReceiptView)}
|
||||||
|
className={`${buttonClassName} ${activeView === item.id ? 'border-brand-primary bg-brand-primary text-brand-contrast' : ''}`}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{activeView === 'new' && (
|
||||||
|
<form onSubmit={handleReceiptSubmit} 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"
|
||||||
|
onClick={() => setSelectedCategory(category.name)}
|
||||||
|
className={`flex min-h-24 flex-col items-center justify-center gap-3 rounded-xl border p-4 text-center font-bold transition-colors cursor-pointer ${
|
||||||
|
selectedCategory === category.name
|
||||||
|
? 'border-brand-primary bg-brand-primary/12 text-brand-primary'
|
||||||
|
: 'border-dark-border bg-dark-input/40 text-dark-text hover:border-brand-primary'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<category.icon className="h-6 w-6" />
|
||||||
|
{category.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 rounded-xl border border-dark-border bg-dark-input/35 p-4">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">2. Detalhes do recebimento</p>
|
||||||
|
<div className="mt-3 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Produto / material
|
||||||
|
<input value={form.product} onChange={(event) => setForm(current => ({ ...current, product: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="ex: Meia malha 30.1" />
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Fornecedor
|
||||||
|
<input value={form.supplier} onChange={(event) => setForm(current => ({ ...current, supplier: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Opcional" />
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Quantidade
|
||||||
|
<input inputMode="decimal" value={form.quantity} onChange={(event) => setForm(current => ({ ...current, quantity: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="ex: 120" />
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Unidade
|
||||||
|
<select value={form.unit} onChange={(event) => setForm(current => ({ ...current, unit: event.target.value }))} className={`${inputClassName} mt-1`}>
|
||||||
|
<option value="kg">kg</option>
|
||||||
|
<option value="rolos">rolos</option>
|
||||||
|
<option value="un.">un.</option>
|
||||||
|
<option value="caixas">caixas</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Nota fiscal
|
||||||
|
<input value={form.invoice} onChange={(event) => setForm(current => ({ ...current, invoice: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="NF-001234" />
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted md:col-span-2">
|
||||||
|
Observações
|
||||||
|
<textarea value={form.notes} onChange={(event) => setForm(current => ({ ...current, notes: event.target.value }))} className="mt-1 min-h-20 w-full rounded-lg border border-dark-border bg-dark-input px-3 py-2 text-sm font-semibold text-dark-text outline-none placeholder:text-dark-muted focus:border-brand-primary" placeholder="Condição, divergências, lote..." />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" className="mt-4 inline-flex h-11 w-full items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-opacity hover:opacity-90 cursor-pointer">
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
Registrar recebimento
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeView === 'pending' && (
|
||||||
|
<ReceiptList
|
||||||
|
receipts={visibleReceipts}
|
||||||
|
emptyTitle="Nenhum recebimento pendente"
|
||||||
|
onApprove={markApproved}
|
||||||
|
onRemove={(receiptId) => saveReceipts(receipts.filter(item => item.id !== receiptId))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{activeView === 'history' && (
|
||||||
|
<ReceiptList
|
||||||
|
receipts={visibleReceipts}
|
||||||
|
emptyTitle="Nenhum recebimento no histórico"
|
||||||
|
onApprove={markApproved}
|
||||||
|
onRemove={(receiptId) => saveReceipts(receipts.filter(item => item.id !== receiptId))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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 = () => {
|
||||||
|
const [plans, setPlans] = useState<FabricPlan[]>(loadFabricPlans);
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
material: '',
|
||||||
|
color: '',
|
||||||
|
quantityKg: '',
|
||||||
|
supplier: '',
|
||||||
|
priority: 'Normal',
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalKg = plans.reduce((total, plan) => total + plan.quantityKg, 0);
|
||||||
|
|
||||||
|
const savePlans = (nextPlans: FabricPlan[]) => {
|
||||||
|
setPlans(nextPlans);
|
||||||
|
localStorage.setItem('nexstar_fabric_plans', JSON.stringify(nextPlans));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const material = form.material.trim();
|
||||||
|
const quantityKg = parseDecimal(form.quantityKg);
|
||||||
|
if (!material || quantityKg <= 0) return;
|
||||||
|
|
||||||
|
const nextPlan: FabricPlan = {
|
||||||
|
id: `${Date.now()}`,
|
||||||
|
material,
|
||||||
|
color: form.color.trim() || 'Todas as cores',
|
||||||
|
quantityKg,
|
||||||
|
supplier: form.supplier.trim() || 'Sem fornecedor',
|
||||||
|
priority: form.priority,
|
||||||
|
};
|
||||||
|
|
||||||
|
savePlans([nextPlan, ...plans]);
|
||||||
|
setForm({ material: '', color: '', quantityKg: '', supplier: '', priority: 'Normal' });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Planos ativos</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.length}</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Kg planejado</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-dark-text">{formatNumber(totalKg)} kg</p>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">Críticos</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-dark-text">{plans.filter(plan => plan.priority === 'Crítico').length}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-[420px_1fr]">
|
||||||
|
<form onSubmit={handleSubmit} className={`${panelClassName} p-5`}>
|
||||||
|
<h2 className="text-base font-bold text-dark-text">Novo plano de malha</h2>
|
||||||
|
<div className="mt-4 grid grid-cols-1 gap-3">
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Malha / tecido
|
||||||
|
<input value={form.material} onChange={(event) => setForm(current => ({ ...current, material: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="ex: Meia malha 30.1" />
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Cor
|
||||||
|
<input value={form.color} onChange={(event) => setForm(current => ({ ...current, color: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Todas as cores" />
|
||||||
|
</label>
|
||||||
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Quantidade kg
|
||||||
|
<input inputMode="decimal" value={form.quantityKg} onChange={(event) => setForm(current => ({ ...current, quantityKg: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="ex: 180" />
|
||||||
|
</label>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Prioridade
|
||||||
|
<select value={form.priority} onChange={(event) => setForm(current => ({ ...current, priority: event.target.value }))} className={`${inputClassName} mt-1`}>
|
||||||
|
<option>Normal</option>
|
||||||
|
<option>Atenção</option>
|
||||||
|
<option>Crítico</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label className="text-xs font-bold text-dark-muted">
|
||||||
|
Fornecedor
|
||||||
|
<input value={form.supplier} onChange={(event) => setForm(current => ({ ...current, supplier: event.target.value }))} className={`${inputClassName} mt-1`} placeholder="Opcional" />
|
||||||
|
</label>
|
||||||
|
<button type="submit" className="mt-2 inline-flex h-11 items-center justify-center gap-2 rounded-lg bg-brand-primary px-4 text-sm font-bold text-brand-contrast transition-opacity hover:opacity-90 cursor-pointer">
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
Salvar plano
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className={`${panelClassName} overflow-hidden`}>
|
||||||
|
<div className="border-b border-dark-border p-5">
|
||||||
|
<h2 className="text-base font-bold text-dark-text">Planos de malha</h2>
|
||||||
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Itens planejados para compra ou recebimento.</p>
|
||||||
|
</div>
|
||||||
|
{plans.length ? (
|
||||||
|
<div className="divide-y divide-dark-border">
|
||||||
|
{plans.map(plan => (
|
||||||
|
<div key={plan.id} className="grid grid-cols-1 gap-3 p-4 md:grid-cols-[1.4fr_1fr_100px_100px_auto] md:items-center">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold text-dark-text">{plan.material}</p>
|
||||||
|
<p className="text-xs font-semibold text-dark-muted">{plan.color}</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-semibold text-dark-muted">{plan.supplier}</p>
|
||||||
|
<p className="text-sm font-bold text-dark-text">{formatNumber(plan.quantityKg)} kg</p>
|
||||||
|
<span className="w-fit rounded-full border border-dark-border bg-dark-input px-2.5 py-1 text-xs font-bold text-dark-text">{plan.priority}</span>
|
||||||
|
<button type="button" onClick={() => savePlans(plans.filter(item => item.id !== plan.id))} className="text-sm font-bold text-red-400 transition-colors hover:text-red-300 cursor-pointer">
|
||||||
|
Remover
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</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">Cadastre o primeiro plano no formulário ao lado.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PurchaseNeedsScreen = () => (
|
||||||
|
<div className={pageClassName}>
|
||||||
|
<Header title="Necessidade de Compra" subtitle="Materiais abaixo do mínimo e necessidade projetada para compra." backTo="/supplies" />
|
||||||
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
||||||
|
{[
|
||||||
|
{ label: 'Itens críticos', value: '0' },
|
||||||
|
{ label: 'Compra sugerida', value: '0 kg' },
|
||||||
|
{ label: 'Fornecedores pendentes', value: '0' },
|
||||||
|
].map(stat => (
|
||||||
|
<div key={stat.label} className="rounded-2xl border border-dark-border bg-dark-card p-5 shadow-sm">
|
||||||
|
<p className="text-xs font-bold uppercase tracking-widest text-dark-muted">{stat.label}</p>
|
||||||
|
<p className="mt-2 text-3xl font-bold text-dark-text">{stat.value}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<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">Necessidade por material</h2>
|
||||||
|
<p className="mt-1 text-sm font-semibold text-dark-muted">Calculada com estoque atual, mínimo configurado e consumo dos planos.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" className={buttonClassName}><Download className="h-4 w-4" /> Exportar CSV</button>
|
||||||
|
</div>
|
||||||
|
<div className={emptyStateClassName}>
|
||||||
|
<BarChart3 className="h-8 w-8 text-brand-primary" />
|
||||||
|
<h3 className="text-base font-bold text-dark-text">Nenhuma necessidade de compra</h3>
|
||||||
|
<p className="text-sm font-semibold text-dark-muted">Configure mínimos e registre estoque para gerar sugestões.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${panelClassName} p-5`}>
|
||||||
|
<h2 className="text-base font-bold text-dark-text">Estoque mínimo por item</h2>
|
||||||
|
<p className="mt-2 text-sm font-semibold text-dark-muted">Defina o ponto de pedido de cada matéria-prima para aparecer aqui quando ficar abaixo do mínimo.</p>
|
||||||
|
<button type="button" className={`${buttonClassName} mt-4`}>Configurar estoques mínimos</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Supplies = () => {
|
||||||
|
const { section } = useParams<{ section?: string }>();
|
||||||
|
|
||||||
|
if (!section) return <SuppliesHub />;
|
||||||
|
if (section === 'inventory') return <InventoryScreen />;
|
||||||
|
if (section === 'fabric-planning') return <FabricPlanningScreen />;
|
||||||
|
if (section === 'purchase-needs') return <PurchaseNeedsScreen />;
|
||||||
|
|
||||||
|
return <Navigate to="/supplies" replace />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Supplies;
|
||||||
Reference in New Issue
Block a user