+ Colunas esperadas na planilha: area, sku, tipo,
+ paletes e opcional acao.
+
+ Se acao for APAGAR/EXCLUIR/DELETE, o registro será removido.
+ Caso contrário, será feito incluir/atualizar.
+
+
+
+
+
+
Estoque por área
+
+
+
+
Área
SKU
Tipo
Paletes
Ações
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Consulta: onde estão os produtos
+
+
+
Área
SKU
Tipo
Quantidade
+
+
+
+
+
+
+
Totais por SKU (todas as áreas)
+
+
+
SKU
Total Paletes
+
+
+
+
+
+
+
+
+
+
Expedir (retirar do estoque)
+
+
+
+
+
Histórico de expedição
+
+
+
Data
SKU
Tipo
Qtd expedida
+
+
+
+
+
+
+
+
+
+
Layout do Estoque (TISSUE | C | B | A | LONIL)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/styles.css b/styles.css
new file mode 100644
index 0000000..f54aa36
--- /dev/null
+++ b/styles.css
@@ -0,0 +1,185 @@
+:root {
+ font-family: Inter, Arial, sans-serif;
+ color: #182028;
+ background: #f5f7fa;
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+}
+
+header {
+ background: #1f4d8f;
+ color: #fff;
+ padding: 1.5rem;
+}
+
+main {
+ max-width: 1100px;
+ margin: 1rem auto 2rem;
+ padding: 0 1rem;
+}
+
+.card {
+ background: #fff;
+ border-radius: 10px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+}
+
+.grid {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.two-col {
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+}
+
+.form-grid {
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ align-items: end;
+}
+
+label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ font-size: 0.9rem;
+}
+
+input, select, button {
+ padding: 0.55rem 0.7rem;
+ border-radius: 8px;
+ border: 1px solid #ccd4dd;
+ font-size: 0.95rem;
+}
+
+button {
+ background: #1f4d8f;
+ color: #fff;
+ border: 0;
+ cursor: pointer;
+}
+
+button.secondary { background: #51606f; }
+button.danger { background: #b93636; }
+button.edit-btn { background: #2a7f46; margin-right: 0.35rem; }
+button.delete-btn { background: #b93636; }
+
+.actions {
+ display: flex;
+ align-items: flex-end;
+}
+
+.tabs {
+ display: flex;
+ gap: 0.5rem;
+ margin-bottom: 1rem;
+}
+
+.tab-btn { background: #8c96a3; }
+.tab-btn.active { background: #1f4d8f; }
+
+.tab-content { display: none; }
+.tab-content.active { display: block; }
+
+.table-wrapper {
+ overflow-x: auto;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+th, td {
+ border-bottom: 1px solid #e1e6eb;
+ padding: 0.55rem;
+ text-align: left;
+}
+
+.status {
+ min-height: 1.3rem;
+ font-weight: 600;
+}
+
+.status.error { color: #b93636; }
+.status.success { color: #2a7f46; }
+
+
+.helper-text {
+ margin-top: 0;
+ color: #4a5563;
+ font-size: 0.9rem;
+ line-height: 1.4;
+}
+
+.layout-actions {
+ display: flex;
+ gap: 0.6rem;
+ margin-top: 0.8rem;
+ flex-wrap: wrap;
+}
+
+.layout-btn { background: #2a7f46 !important; }
+
+.layout-container {
+ position: fixed;
+ inset: 0;
+ background: #f4f6f8;
+ overflow: auto;
+ padding: 1rem;
+ z-index: 1000;
+}
+
+.layout-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 0.8rem;
+ background: #fff;
+ border-radius: 8px;
+ padding: 0.8rem;
+ margin-bottom: 1rem;
+}
+
+.layout-header-actions {
+ display: flex;
+ gap: 0.5rem;
+}
+
+.hidden { display: none !important; }
+
+.layout-grid {
+ display: grid;
+ grid-template-columns: 120px repeat(3, minmax(180px, 1fr)) 120px;
+ gap: 8px;
+}
+
+.celula {
+ border: 2px solid #444;
+ min-height: 78px;
+ padding: 6px;
+ font-size: 12px;
+ background: #f5f5f5;
+}
+
+.vazio { background: #e0e0e0; }
+.ocupado { background: #90caf9; }
+
+.sku {
+ font-size: 11px;
+ margin-top: 4px;
+ border-radius: 4px;
+ padding: 4px;
+ color: #fff;
+ font-weight: 600;
+}
+
+.material-pl2 { background: #1f4d8f; }
+.material-pbr { background: #2a7f46; }
+.material-fardo { background: #6c757d; }
diff --git a/supabase/alter_tables_from_current_schema.sql b/supabase/alter_tables_from_current_schema.sql
new file mode 100644
index 0000000..4c64125
--- /dev/null
+++ b/supabase/alter_tables_from_current_schema.sql
@@ -0,0 +1,69 @@
+-- ==========================================================
+-- WMSS - ALTER TABLEs para schema atual informado pelo usuário
+-- Base atual:
+-- estoque_area(area, sku, paletes, tipo)
+-- movimentacoes(id, sku, tipo, paletes, created_at)
+-- produtos(sku, fardos_por_palete, tipo)
+-- tipos_palete(tipo, fardos_por_palete)
+-- ==========================================================
+
+-- 1) Regras mínimas de qualidade de dados
+alter table if exists public.estoque_area
+ add constraint if not exists estoque_area_paletes_ck check (paletes >= 0);
+
+alter table if exists public.movimentacoes
+ add constraint if not exists movimentacoes_paletes_ck check (paletes > 0);
+
+alter table if exists public.produtos
+ add constraint if not exists produtos_fardos_ck check (fardos_por_palete > 0);
+
+-- 2) Índices úteis para consulta/performance
+create index if not exists idx_estoque_area_sku on public.estoque_area (sku);
+create index if not exists idx_estoque_area_area on public.estoque_area (area);
+create index if not exists idx_movimentacoes_created_at on public.movimentacoes (created_at desc);
+create index if not exists idx_movimentacoes_sku on public.movimentacoes (sku);
+
+-- 3) Colunas opcionais para registrar expedição com mais contexto
+-- (o front já funciona sem elas; isso é apenas melhoria)
+alter table if exists public.movimentacoes
+ add column if not exists operacao text;
+
+alter table if exists public.movimentacoes
+ add column if not exists area text;
+
+alter table if exists public.movimentacoes
+ add column if not exists observacao text;
+
+-- 4) Default e check da coluna operacao (somente se a coluna existir)
+do $$
+begin
+ if exists (
+ select 1
+ from information_schema.columns
+ where table_schema = 'public'
+ and table_name = 'movimentacoes'
+ and column_name = 'operacao'
+ ) then
+ alter table public.movimentacoes
+ alter column operacao set default 'EXPEDICAO';
+
+ if not exists (
+ select 1
+ from pg_constraint
+ where conname = 'movimentacoes_operacao_ck'
+ ) then
+ alter table public.movimentacoes
+ add constraint movimentacoes_operacao_ck
+ check (operacao in ('ENTRADA', 'EXPEDICAO', 'AJUSTE'));
+ end if;
+ end if;
+end $$;
+
+-- 5) View de totais por SKU para uso em consulta/relatórios
+create or replace view public.vw_totais_sku as
+select
+ sku,
+ sum(paletes)::integer as total_paletes
+from public.estoque_area
+group by sku
+order by sku;
diff --git a/supabase/schema_v2.sql b/supabase/schema_v2.sql
new file mode 100644
index 0000000..c6cad51
--- /dev/null
+++ b/supabase/schema_v2.sql
@@ -0,0 +1,221 @@
+-- ==========================================================
+-- WMSS - Schema SQL v2 (Supabase / PostgreSQL)
+-- Objetivo:
+-- - Cadastro de áreas e SKUs
+-- - Controle de estoque por área + SKU + tipo
+-- - Expedição com baixa de estoque e log de movimentações
+-- - Consultas consolidadas (totais por área e por SKU)
+-- ==========================================================
+
+-- Extensão para UUID aleatório
+create extension if not exists pgcrypto;
+
+-- -----------------------------
+-- Tabelas de domínio
+-- -----------------------------
+create table if not exists public.areas (
+ codigo text primary key,
+ descricao text,
+ ativo boolean not null default true,
+ created_at timestamp without time zone not null default now(),
+ updated_at timestamp without time zone not null default now(),
+ constraint areas_codigo_ck check (codigo = upper(trim(codigo)) and length(trim(codigo)) > 0)
+);
+
+create table if not exists public.tipos_palete (
+ tipo text primary key,
+ fardos_por_palete integer,
+ created_at timestamp without time zone not null default now(),
+ constraint tipos_palete_tipo_ck check (tipo = upper(trim(tipo)) and length(trim(tipo)) > 0),
+ constraint tipos_palete_fardos_ck check (fardos_por_palete is null or fardos_por_palete > 0)
+);
+
+create table if not exists public.produtos (
+ sku bigint primary key,
+ descricao text,
+ fardos_por_palete integer not null,
+ created_at timestamp without time zone not null default now(),
+ updated_at timestamp without time zone not null default now(),
+ constraint produtos_sku_ck check (sku > 0),
+ constraint produtos_fardos_ck check (fardos_por_palete > 0)
+);
+
+-- -----------------------------
+-- Estoque por área
+-- -----------------------------
+create table if not exists public.estoque_area (
+ area text not null,
+ sku bigint not null,
+ tipo text not null,
+ paletes integer not null,
+ created_at timestamp without time zone not null default now(),
+ updated_at timestamp without time zone not null default now(),
+ constraint estoque_area_pk primary key (area, sku, tipo),
+ constraint estoque_area_area_fk foreign key (area) references public.areas (codigo) on update cascade,
+ constraint estoque_area_sku_fk foreign key (sku) references public.produtos (sku) on update cascade,
+ constraint estoque_area_tipo_fk foreign key (tipo) references public.tipos_palete (tipo) on update cascade,
+ constraint estoque_area_paletes_ck check (paletes >= 0)
+);
+
+create index if not exists idx_estoque_area_sku on public.estoque_area (sku);
+create index if not exists idx_estoque_area_area on public.estoque_area (area);
+
+-- -----------------------------
+-- Movimentações
+-- -----------------------------
+create table if not exists public.movimentacoes (
+ id uuid primary key default gen_random_uuid(),
+ operacao text not null,
+ area text not null,
+ sku bigint not null,
+ tipo text not null,
+ paletes integer not null,
+ observacao text,
+ created_at timestamp without time zone not null default now(),
+ constraint movimentacoes_operacao_ck check (operacao in ('ENTRADA', 'EXPEDICAO', 'AJUSTE')),
+ constraint movimentacoes_paletes_ck check (paletes > 0),
+ constraint movimentacoes_area_fk foreign key (area) references public.areas (codigo) on update cascade,
+ constraint movimentacoes_sku_fk foreign key (sku) references public.produtos (sku) on update cascade,
+ constraint movimentacoes_tipo_fk foreign key (tipo) references public.tipos_palete (tipo) on update cascade
+);
+
+create index if not exists idx_movimentacoes_data on public.movimentacoes (created_at desc);
+create index if not exists idx_movimentacoes_sku on public.movimentacoes (sku);
+
+-- -----------------------------
+-- Trigger para updated_at
+-- -----------------------------
+create or replace function public.fn_set_updated_at()
+returns trigger
+language plpgsql
+as $$
+begin
+ new.updated_at = now();
+ return new;
+end;
+$$;
+
+drop trigger if exists trg_areas_updated_at on public.areas;
+create trigger trg_areas_updated_at
+before update on public.areas
+for each row execute function public.fn_set_updated_at();
+
+drop trigger if exists trg_produtos_updated_at on public.produtos;
+create trigger trg_produtos_updated_at
+before update on public.produtos
+for each row execute function public.fn_set_updated_at();
+
+drop trigger if exists trg_estoque_area_updated_at on public.estoque_area;
+create trigger trg_estoque_area_updated_at
+before update on public.estoque_area
+for each row execute function public.fn_set_updated_at();
+
+-- -----------------------------
+-- Função de expedição (baixa segura)
+-- -----------------------------
+create or replace function public.fn_expedir_produto(
+ p_area text,
+ p_sku bigint,
+ p_tipo text,
+ p_paletes integer,
+ p_observacao text default null
+)
+returns table (
+ area text,
+ sku bigint,
+ tipo text,
+ saldo_paletes integer
+)
+language plpgsql
+as $$
+declare
+ v_atual integer;
+ v_novo integer;
+begin
+ if p_paletes is null or p_paletes <= 0 then
+ raise exception 'Quantidade para expedição deve ser maior que zero';
+ end if;
+
+ select e.paletes
+ into v_atual
+ from public.estoque_area e
+ where e.area = upper(trim(p_area))
+ and e.sku = p_sku
+ and e.tipo = upper(trim(p_tipo))
+ for update;
+
+ if v_atual is null then
+ raise exception 'Registro não encontrado no estoque (área %, sku %, tipo %)', p_area, p_sku, p_tipo;
+ end if;
+
+ if v_atual < p_paletes then
+ raise exception 'Estoque insuficiente para expedição. Atual: %, solicitado: %', v_atual, p_paletes;
+ end if;
+
+ v_novo := v_atual - p_paletes;
+
+ if v_novo = 0 then
+ delete from public.estoque_area
+ where area = upper(trim(p_area))
+ and sku = p_sku
+ and tipo = upper(trim(p_tipo));
+ else
+ update public.estoque_area
+ set paletes = v_novo
+ where area = upper(trim(p_area))
+ and sku = p_sku
+ and tipo = upper(trim(p_tipo));
+ end if;
+
+ insert into public.movimentacoes (operacao, area, sku, tipo, paletes, observacao)
+ values ('EXPEDICAO', upper(trim(p_area)), p_sku, upper(trim(p_tipo)), p_paletes, p_observacao);
+
+ return query
+ select upper(trim(p_area)), p_sku, upper(trim(p_tipo)), v_novo;
+end;
+$$;
+
+-- -----------------------------
+-- Views para consulta
+-- -----------------------------
+create or replace view public.vw_consulta_estoque as
+select
+ e.area,
+ e.sku,
+ p.descricao as produto,
+ e.tipo,
+ e.paletes,
+ coalesce(tp.fardos_por_palete, p.fardos_por_palete) as fardos_por_palete_ref,
+ e.paletes * coalesce(tp.fardos_por_palete, p.fardos_por_palete) as total_fardos_estimado
+from public.estoque_area e
+join public.produtos p on p.sku = e.sku
+left join public.tipos_palete tp on tp.tipo = e.tipo;
+
+create or replace view public.vw_totais_sku as
+select
+ e.sku,
+ p.descricao as produto,
+ sum(e.paletes)::integer as total_paletes,
+ sum(e.paletes * coalesce(tp.fardos_por_palete, p.fardos_por_palete))::integer as total_fardos_estimado
+from public.estoque_area e
+join public.produtos p on p.sku = e.sku
+left join public.tipos_palete tp on tp.tipo = e.tipo
+group by e.sku, p.descricao
+order by e.sku;
+
+-- -----------------------------
+-- Seeds básicos (idempotentes)
+-- -----------------------------
+insert into public.tipos_palete (tipo, fardos_por_palete)
+values
+ ('PL2', null),
+ ('PBR', null),
+ ('FARDO', 1)
+on conflict (tipo) do nothing;
+
+-- Exemplo de áreas
+insert into public.areas (codigo, descricao)
+values
+ ('B01', 'Área B01'),
+ ('B02', 'Área B02')
+on conflict (codigo) do nothing;