-
Disponíveis {{ availableOrdersCount }} Pedidos
-
+
Disponíveis {{ availableOrdersCount }} Documentos
+
-
Carregando pedidos...
+
Carregando pedidos e NFs de venda futura...
-
-
+
+ Nenhum pedido ou NF de venda futura disponível. Use os filtros acima para buscar.
+
+
-
+
-
\ No newline at end of file
+
+
+
+
+ {{ group.fatura ? 'NF venda futura' : 'Pedido' }}
+ {{ group.docNum }} · {{ group.items[0]?.CardName || 'N/A' }}
+
+ {{ group.items[0]?.Name || 'Sem localidade' }} · {{ group.items[0]?.SlpName || 'Sem vendedor' }}
+ · Frete {{ group.totalFrete | currency:'BRL' }}
+
+
+
diff --git a/src/app/modulos/ordem-carregamento/componentes/dual-list-box/dual-list-box.component.scss b/src/app/modulos/ordem-carregamento/componentes/dual-list-box/dual-list-box.component.scss
index d73fbae3..e8545931 100644
--- a/src/app/modulos/ordem-carregamento/componentes/dual-list-box/dual-list-box.component.scss
+++ b/src/app/modulos/ordem-carregamento/componentes/dual-list-box/dual-list-box.component.scss
@@ -53,7 +53,7 @@
flex: 1;
display: flex;
flex-direction: column;
- height: 400px;
+ height: max(400px, calc(100vh - 360px));
overflow: hidden;
&.collapsed {
@@ -131,7 +131,8 @@
}
.order-header {
- background-color: #ffc107;
+ background-color: #fff8e1;
+ border-left: 4px solid #ffc107;
color: #333;
padding: 8px 16px;
font-weight: 500;
@@ -140,9 +141,31 @@
font-size: 14px;
}
+.order-header.fatura {
+ background-color: #e3f2fd;
+ border-left-color: #1976d2;
+}
+
.order-info {
white-space: normal;
- max-width: 50%;
+ flex: 1;
+ min-width: 0;
+}
+
+.empty-state {
+ color: #6c757d;
+ text-align: center;
+ padding: 32px 16px;
+ margin: 0;
+}
+
+.total-badge {
+ font-size: 13px;
+ padding: 6px 8px;
+
+ & + & {
+ margin-left: 8px;
+ }
}
.stock-info {
@@ -153,37 +176,9 @@
white-space: nowrap;
}
-.stock-green {
- background-color: #28a745;
- color: #ffffff;
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 12px;
-}
-.stock-gray {
- background-color: #6c757d;
- color: #ffffff;
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 12px;
-}
-.stock-orange {
- background-color: #e65100;
- color: #ffffff;
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 12px;
-}
-.stock-purple {
- background-color: #9c27b0;
- color: #ffffff;
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 12px;
-}
.order-list {
list-style: none;
@@ -264,7 +259,8 @@
/* ... (estilos anteriores mantidos) ... */
.order-header {
- background-color: #ffc107;
+ background-color: #fff8e1;
+ border-left: 4px solid #ffc107;
color: #333;
padding: 8px 12px;
font-weight: 500;
@@ -375,4 +371,69 @@
padding: 2px 4px;
}
}
-}
\ No newline at end of file
+}
+.retirada {
+ font-weight: 600;
+ background-color: #e3f2fd;
+ border-radius: 4px;
+ padding: 2px 6px;
+
+ input {
+ width: 90px;
+ margin: 0 4px;
+ }
+}
+
+.capacidade {
+ input {
+ width: 120px;
+ }
+
+ .progress {
+ height: 18px;
+ }
+}
+
+.estoque-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ border: 1px solid transparent;
+ border-radius: 12px;
+ padding: 2px 10px;
+ font-size: 12px;
+
+ b {
+ color: inherit;
+ }
+
+ &.estoque {
+ background-color: #e6f4ea;
+ border-color: #a8d5b5;
+ color: #1e7e34;
+ }
+
+ &.producao {
+ background-color: #f1f3f5;
+ border-color: #ced4da;
+ color: #495057;
+ }
+
+ &.carregamento {
+ background-color: #fff1e6;
+ border-color: #ffc59e;
+ color: #b54708;
+ }
+
+ &.balanco {
+ background-color: #f3e8fd;
+ border-color: #d4b0f5;
+ color: #6f2da8;
+ }
+
+ &.negativo {
+ background-color: #fdecea;
+ border-color: #f5c2c7;
+ color: #b02a37;
+ }
+}
diff --git a/src/app/modulos/ordem-carregamento/componentes/dual-list-box/dual-list-box.component.ts b/src/app/modulos/ordem-carregamento/componentes/dual-list-box/dual-list-box.component.ts
index 1b95cd4f..d4d51eff 100644
--- a/src/app/modulos/ordem-carregamento/componentes/dual-list-box/dual-list-box.component.ts
+++ b/src/app/modulos/ordem-carregamento/componentes/dual-list-box/dual-list-box.component.ts
@@ -1,7 +1,10 @@
-import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { PedidoVenda } from '../../../../sap/model/document/pedido-venda.model';
import { AlertService } from '../../../../shared/service/alert.service';
import { OrdemCarregamentoService } from '../../service/ordem-carregamento.service';
+import { chaveDocumento, isFaturaReserva } from '../../models/fatura-reserva';
+
+type Grupo = { key: string; docNum: number; fatura: boolean; items: PedidoVenda[]; totalFrete: number };
@Component({
selector: 'app-dual-list-box',
@@ -12,13 +15,16 @@ export class DualListBoxComponent {
@Input() availableItems: PedidoVenda[] = [];
@Input() selectedItems: PedidoVenda[] = [];
@Input() showStock: boolean = false;
- @Input() nextLink: string = '';
+ // Ha mais pagina de pedidos ou de NF de venda futura: mostra "Carregar Mais".
+ @Input() temMais: boolean = false;
@Input() isLoadingOrders: boolean = false;
@Output() selectedItemsChange = new EventEmitter
();
+ // Devolve ao formulario a lista de disponiveis depois de mover itens: o "Carregar Mais" parte dela.
+ @Output() availableItemsChange = new EventEmitter();
@Output() loadMore = new EventEmitter();
quantidadesEmCarregamento: { [itemCode: string]: number } = {};
- isLoading: { [itemCode: string]: boolean } = {};
+ carregandoEstoque = false;
searchTermAvailable: string = '';
searchTermSelected: string = '';
carregamentoPorPedido: boolean = true;
@@ -29,12 +35,28 @@ export class DualListBoxComponent {
private ordemCarregamentoService: OrdemCarregamentoService
) {}
- ngOnChanges(): void {
- if (this.showStock && this.availableItems.length > 0) {
+ ngOnChanges(changes: SimpleChanges): void {
+ // So ao ligar o estoque ou chegar busca nova; mover item entre as listas nao recarrega.
+ // A lista que volta pelo availableItemsChange nao e busca nova.
+ const buscaNova = changes['availableItems'] && changes['availableItems'].currentValue !== this.disponiveisEmitidos;
+ const mudou = changes['showStock'] || buscaNova;
+ if (mudou && this.showStock && this.availableItems.length > 0) {
this.loadQuantidadesEmCarregamento();
}
}
+ // Opcional; gravado na ordem (U_capacidadeCaminhao).
+ @Input() capacidadeKg: number | null = null;
+ @Output() capacidadeKgChange = new EventEmitter();
+
+ balanco(item: PedidoVenda): number {
+ return Number(item.OnHand || 0) - (this.quantidadesEmCarregamento[item.ItemCode] ?? 0);
+ }
+
+ get ocupacao(): number {
+ return this.capacidadeKg > 0 ? Math.round(this.totalSelectedWeight / this.capacidadeKg * 100) : 0;
+ }
+
get totalSelectedWeight(): number {
return this.selectedItems.reduce(
(sum, item) => sum + (Number(item.Weight1) || 0),
@@ -43,20 +65,18 @@ export class DualListBoxComponent {
}
get availableOrdersCount(): number {
- const uniqueDocNums = new Set(this.availableItems.map(item => item.DocNum));
- return uniqueDocNums.size;
+ return new Set(this.availableItems.map(chaveDocumento)).size;
}
get selectedOrdersCount(): number {
- const uniqueDocNums = new Set(this.selectedItems.map(item => item.DocNum));
- return uniqueDocNums.size;
+ return new Set(this.selectedItems.map(chaveDocumento)).size;
}
- get groupedAvailableItems(): { docNum: number; items: PedidoVenda[], totalFrete: number }[] {
+ get groupedAvailableItems(): Grupo[] {
return this.groupItems(this.availableItems, this.searchTermAvailable);
}
- get groupedSelectedItems(): { docNum: number; items: PedidoVenda[], totalFrete: number }[] {
+ get groupedSelectedItems(): Grupo[] {
return this.groupItems(this.selectedItems, this.searchTermSelected);
}
@@ -64,82 +84,144 @@ export class DualListBoxComponent {
return this.selectedItems.reduce((sum, item) => sum + (Number(item.DistribSum) || 0), 0);
}
- selectGroup(group: { docNum: number, items: PedidoVenda[] }): void {
+ selectGroup(group: Grupo): void {
const itemsToMove = [...group.items];
this.selectedItems = this.sortItems([...this.selectedItems, ...itemsToMove]);
- this.availableItems = this.availableItems.filter(i => i.DocNum !== group.docNum);
+ this.atualizarDisponiveis(this.availableItems.filter(i => chaveDocumento(i) !== group.key));
this.selectedItemsChange.emit(this.selectedItems);
}
- trackByDocNum(index: number, group: { docNum: number }) {
- return group.docNum;
+ trackByDocNum(index: number, group: Grupo) {
+ return group.key;
}
trackByItem(index: number, item: PedidoVenda) {
- return `${item.DocNum}-${item.ItemCode}`;
+ return `${chaveDocumento(item)}-${item.ItemCode}-${item.BaseLine}`;
}
- removeGroup(group: { docNum: number, items: PedidoVenda[] }): void {
+ removeGroup(group: Grupo): void {
const itemsToMove = [...group.items];
- this.availableItems = this.sortItems([...this.availableItems, ...itemsToMove]);
- this.selectedItems = this.selectedItems.filter(i => i.DocNum !== group.docNum);
+ itemsToMove.forEach(i => this.restaurarSaldo(i));
+ this.atualizarDisponiveis(this.sortItems([...this.availableItems, ...itemsToMove]));
+ this.selectedItems = this.selectedItems.filter(i => chaveDocumento(i) !== group.key);
+ this.selectedItemsChange.emit(this.selectedItems);
+ }
+
+ isFaturaReserva(item: PedidoVenda): boolean {
+ return isFaturaReserva(item);
+ }
+
+ // NF de venda futura sai aos poucos: a quantidade da linha selecionada e a retirada desta ordem,
+ // limitada ao SaldoFatura que vem do backend. Peso acompanha a quantidade.
+ alterarRetirada(item: PedidoVenda, valor: any): void {
+ const pesoUnitario = this.pesoUnitario(item);
+ const saldo = item.SaldoFatura ?? Number(item.Quantity);
+ let quantidade = Math.max(0, Number(valor) || 0);
+ if (quantidade > saldo) {
+ quantidade = saldo;
+ this.alertService.error(`Saldo da NF de venda futura para ${item.ItemCode} é ${saldo}.`);
+ }
+ const freteUnitario = this.freteUnitario(item);
+ item.Quantity = quantidade;
+ item.Weight1 = quantidade * pesoUnitario;
+ item.DistribSum = this.freteDe(item, quantidade, freteUnitario);
this.selectedItemsChange.emit(this.selectedItems);
}
- private groupItems(items: PedidoVenda[], searchTerm: string): { docNum: number; items: PedidoVenda[], totalFrete: number }[] {
+ private pesoUnitario(item: PedidoVenda): number {
+ item.pesoUnitario ??= Number(item.Quantity) ? Number(item.Weight1) / Number(item.Quantity) : 0;
+ return item.pesoUnitario;
+ }
+
+ // Frete da NF de venda futura e proporcional a quantidade retirada (vem calculado do backend).
+ private freteUnitario(item: PedidoVenda): number {
+ //Do backend, sem arredondar; dividir o DistribSum (em centavos) zeraria frete pequeno.
+ item.freteUnitario ??= item.FreteUnitario ?? (Number(item.Quantity) ? Number(item.DistribSum || 0) / Number(item.Quantity) : 0);
+ return item.freteUnitario;
+ }
+
+ // Mesmo teto da entrega: o frete da retirada nunca passa do que resta do frete da NF.
+ private freteDe(item: PedidoVenda, quantidade: number, freteUnitario: number): number {
+ const teto = item.FreteRestante ?? Number.POSITIVE_INFINITY;
+ return Math.round(Math.min(quantidade * freteUnitario, teto) * 100) / 100;
+ }
+
+ private restaurarSaldo(item: PedidoVenda): void {
+ if (isFaturaReserva(item) && item.SaldoFatura != null) {
+ const pesoUnitario = this.pesoUnitario(item);
+ const freteUnitario = this.freteUnitario(item);
+ item.Quantity = item.SaldoFatura;
+ item.Weight1 = item.SaldoFatura * pesoUnitario;
+ item.DistribSum = this.freteDe(item, item.SaldoFatura, freteUnitario);
+ }
+ }
+
+ private groupItems(items: PedidoVenda[], searchTerm: string): Grupo[] {
const grouped = items.reduce((acc, item) => {
- const docNum = item.DocNum;
- if (!acc[docNum]) {
- acc[docNum] = { docNum, items: [], totalFrete: 0 };
+ const key = chaveDocumento(item);
+ if (!acc[key]) {
+ acc[key] = { key, docNum: item.DocNum, fatura: isFaturaReserva(item), items: [], totalFrete: 0 };
}
- acc[docNum].items.push(item);
-
- acc[docNum].totalFrete += Number(item.DistribSum) || 0;
-
+ acc[key].items.push(item);
+ acc[key].totalFrete += Number(item.DistribSum) || 0;
return acc;
- }, {} as { [key: number]: { docNum: number; items: PedidoVenda[], totalFrete: number } });
+ }, {} as { [key: string]: Grupo });
+ const termo = this.semAcento(searchTerm);
return Object.values(grouped)
.filter(group => group.items.some(item =>
- `${item.ItemCode || ''} ${item.Dscription || ''} ${item.Name || ''} ${group.docNum}`
- .toLowerCase()
- .includes(searchTerm.toLowerCase())
+ this.semAcento(`${group.docNum} ${item.CardCode || ''} ${item.CardName || ''} ${item.SlpName || ''} ${item.Name || ''} ${item.ItemCode || ''} ${item.Dscription || ''}`)
+ .includes(termo)
))
.sort((a, b) => a.docNum - b.docNum);
}
+ private semAcento(texto: string): string {
+ return (texto || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
+ }
+
+ private disponiveisEmitidos: PedidoVenda[] | null = null;
+
+ private atualizarDisponiveis(itens: PedidoVenda[]): void {
+ this.availableItems = itens;
+ this.disponiveisEmitidos = itens;
+ this.availableItemsChange.emit(itens);
+ }
+
private sortItems(items: PedidoVenda[]): PedidoVenda[] {
return items.sort((a, b) => a.DocNum - b.DocNum || a.ItemCode.localeCompare(b.ItemCode));
}
selectItem(item: PedidoVenda): void {
const itemsToMove = this.carregamentoPorPedido
- ? this.availableItems.filter(i => i.DocNum == item.DocNum)
+ ? this.availableItems.filter(i => chaveDocumento(i) == chaveDocumento(item))
: [item];
this.selectedItems = this.sortItems([...this.selectedItems, ...itemsToMove]);
- this.availableItems = this.sortItems(this.availableItems.filter(i => !itemsToMove.includes(i)));
+ this.atualizarDisponiveis(this.sortItems(this.availableItems.filter(i => !itemsToMove.includes(i))));
this.selectedItemsChange.emit(this.selectedItems);
}
removeItem(item: PedidoVenda): void {
const itemsToRemove = this.carregamentoPorPedido
- ? this.selectedItems.filter(i => i.DocNum == item.DocNum)
+ ? this.selectedItems.filter(i => chaveDocumento(i) == chaveDocumento(item))
: [item];
- this.availableItems = this.sortItems([...this.availableItems, ...itemsToRemove]);
+ itemsToRemove.forEach(i => this.restaurarSaldo(i));
+ this.atualizarDisponiveis(this.sortItems([...this.availableItems, ...itemsToRemove]));
this.selectedItems = this.sortItems(this.selectedItems.filter(i => !itemsToRemove.includes(i)));
this.selectedItemsChange.emit(this.selectedItems);
}
selectAll(): void {
this.selectedItems = this.sortItems([...this.selectedItems, ...this.availableItems]);
- this.availableItems = [];
+ this.atualizarDisponiveis([]);
this.selectedItemsChange.emit(this.selectedItems);
}
removeAll(): void {
this.alertService.confirm('Deseja realmente remover todos os itens selecionados?').then(result => {
if (result.isConfirmed) {
- this.availableItems = this.sortItems([...this.availableItems, ...this.selectedItems]);
+ this.selectedItems.forEach(i => this.restaurarSaldo(i));
+ this.atualizarDisponiveis(this.sortItems([...this.availableItems, ...this.selectedItems]));
this.selectedItems = [];
this.selectedItemsChange.emit(this.selectedItems);
}
@@ -154,24 +236,20 @@ export class DualListBoxComponent {
this.loadMore.emit();
}
+ // Uma consulta traz todos os itens das ordens Abertas (antes era uma chamada por item).
private loadQuantidadesEmCarregamento(): void {
- this.availableItems.forEach(item => {
- if (item.ItemCode && !this.isLoading[item.ItemCode]) {
- this.fetchQuantidadeEmCarregamento(item);
- }
- });
- }
-
- private fetchQuantidadeEmCarregamento(item: PedidoVenda): void {
- this.isLoading[item.ItemCode] = true;
- this.ordemCarregamentoService.getEstoqueEmCarregamento(item.ItemCode).subscribe({
- next: (quantidade) => {
- this.quantidadesEmCarregamento[item.ItemCode] = quantidade;
- this.isLoading[item.ItemCode] = false;
+ if (this.carregandoEstoque) {
+ return;
+ }
+ this.carregandoEstoque = true;
+ this.ordemCarregamentoService.getEstoqueEmCarregamentoItens().subscribe({
+ next: (porItem) => {
+ this.quantidadesEmCarregamento = porItem;
+ this.carregandoEstoque = false;
},
error: () => {
- this.quantidadesEmCarregamento[item.ItemCode] = 0;
- this.isLoading[item.ItemCode] = false;
+ this.alertService.error('Erro ao carregar o estoque em carregamento.');
+ this.carregandoEstoque = false;
}
});
}
diff --git a/src/app/modulos/ordem-carregamento/componentes/formulario/formulario.component.html b/src/app/modulos/ordem-carregamento/componentes/formulario/formulario.component.html
index 7022cc46..c31c5894 100644
--- a/src/app/modulos/ordem-carregamento/componentes/formulario/formulario.component.html
+++ b/src/app/modulos/ordem-carregamento/componentes/formulario/formulario.component.html
@@ -8,14 +8,12 @@
-
-
-
-
+
+