Align cutting families with Olist production data
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 1m17s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 1m17s
This commit is contained in:
@@ -489,6 +489,22 @@ const initDB = async () => {
|
|||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
await pool.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS production_yield_supplier_recommendations (
|
||||||
|
family_key VARCHAR(20) NOT NULL,
|
||||||
|
supplier VARCHAR(255) NOT NULL,
|
||||||
|
sample_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
yield_sample_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
roll_sample_count INTEGER NOT NULL DEFAULT 0,
|
||||||
|
avg_yield_pieces_per_kg NUMERIC(14, 4),
|
||||||
|
avg_kg_per_roll NUMERIC(14, 4),
|
||||||
|
recommended_units_per_roll NUMERIC(14, 4),
|
||||||
|
last_completed_date DATE,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (family_key, supplier)
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
ALTER TABLE production_orders
|
ALTER TABLE production_orders
|
||||||
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'America/Sao_Paulo',
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ DATE_NUM_FMT_IDS = {
|
|||||||
57,
|
57,
|
||||||
}
|
}
|
||||||
|
|
||||||
CUT_SHEETS = {"BLCS", "BLMC", "BLOS", "BLPM"}
|
CUT_SHEETS = {"BLCS", "BLMC", "BLOS", "BLPM", "BLRM"}
|
||||||
COLOR_WORDS = {
|
COLOR_WORDS = {
|
||||||
"amarelo",
|
"amarelo",
|
||||||
"azul",
|
"azul",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { pool } = require('../db');
|
const { pool } = require('../db');
|
||||||
|
|
||||||
const FAMILY_KEYS = ['BLCS', 'BLOS', 'BLMC', 'BLPM'];
|
const FAMILY_KEYS = ['BLCS', 'BLOS', 'BLMC', 'BLPM', 'BLRM'];
|
||||||
const FAMILY_KEY_SET = new Set(FAMILY_KEYS);
|
const FAMILY_KEY_SET = new Set(FAMILY_KEYS);
|
||||||
const PRODUCT_TYPE_KEYS = [
|
const PRODUCT_TYPE_KEYS = [
|
||||||
'finished_apparel',
|
'finished_apparel',
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const { pool } = require('../db');
|
const { pool } = require('../db');
|
||||||
|
|
||||||
const FAMILY_KEYS = new Set(['BLCS', 'BLOS', 'BLMC', 'BLPM']);
|
const FAMILY_KEYS = new Set(['BLCS', 'BLOS', 'BLMC', 'BLPM', 'BLRM']);
|
||||||
const REQUIRED_HEADERS = ['id_op', 'numero_op', 'sku_produto', 'descricao_produto', 'quantidade_produzida', 'data_emissao', 'data_conclusao', 'situacao', 'observacoes'];
|
const REQUIRED_HEADERS = ['id_op', 'numero_op', 'sku_produto', 'descricao_produto', 'quantidade_produzida', 'data_emissao', 'data_conclusao', 'situacao', 'observacoes'];
|
||||||
|
|
||||||
const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
const normalizeText = (value) => String(value || '').replace(/\s+/g, ' ').trim();
|
||||||
@@ -76,12 +76,18 @@ const extractObservationFields = (notes) => {
|
|||||||
const raw = String(notes || '');
|
const raw = String(notes || '');
|
||||||
const lineValue = (pattern) => raw.match(pattern)?.[1]?.trim() || '';
|
const lineValue = (pattern) => raw.match(pattern)?.[1]?.trim() || '';
|
||||||
const supplier = lineValue(/(?:^|\n)\s*FORNECEDOR\s*:\s*([^\r\n]+)/i);
|
const supplier = lineValue(/(?:^|\n)\s*FORNECEDOR\s*:\s*([^\r\n]+)/i);
|
||||||
const lotCode = lineValue(/(?:^|\n)\s*LOTE\s*:\s*([^\r\n]+)/i);
|
const lotCode = lineValue(/(?:^|\n)\s*(?:-\s*)?LOTE\s*:?[\t ]*([^\r\n]+)/i);
|
||||||
const yieldPiecesPerKg = normalizeNumber(raw.match(/RENDIMENTO\s*:\s*([\d.,]+)/i)?.[1]);
|
const yieldMatch = raw.match(/RENDIMENTO\s*:\s*([\d.,]+)(?:\s*[-–]\s*([\d.,]+)\s*KG)?/i);
|
||||||
const explicitRolls = normalizeNumber(raw.match(/(?:QUANTIDADE\s+DE\s+ROLOS|ROLOS|BOBINAS)\s*:\s*([\d.,]+)/i)?.[1]);
|
const yieldPiecesPerKg = normalizeNumber(yieldMatch?.[1]);
|
||||||
|
const bobbinMatch = raw.match(/(?:QUANTIDADE\s+DE\s+ROLOS|ROLOS|BOBINAS)\s*:\s*(\d+(?:[.,]\d+)?)(?:\s*\(\s*X\s*(\d+(?:[.,]\d+)?)\s*\))?(?:\s*[-–]\s*([\d.,]+)\s*KG)?/i);
|
||||||
|
const rollBase = normalizeNumber(bobbinMatch?.[1]);
|
||||||
|
const rollMultiplier = normalizeNumber(bobbinMatch?.[2]) || 1;
|
||||||
|
const explicitRolls = rollBase ? rollBase * rollMultiplier : null;
|
||||||
const fabricKg = normalizeNumber(raw.match(/(?:QUILOS?\s+DE\s+MALHA|QUILOS?\s+MALHA)\s*:\s*([\d.,]+)(?:\s*KG)?/i)?.[1])
|
const fabricKg = normalizeNumber(raw.match(/(?:QUILOS?\s+DE\s+MALHA|QUILOS?\s+MALHA)\s*:\s*([\d.,]+)(?:\s*KG)?/i)?.[1])
|
||||||
|| normalizeNumber(raw.match(/(?:ROLOS|BOBINAS)\s*:\s*[\d.,]+\s*[-–]\s*([\d.,]+)\s*KG/i)?.[1]);
|
|| normalizeNumber(bobbinMatch?.[3])
|
||||||
const ribKg = normalizeNumber(raw.match(/(?:QUILOS?\s+DE\s+(?:GOLA|RIBANA|VIEZ(?:\s*\([^)]*\))?))\s*:\s*([\d.,]+)(?:\s*KG)?/i)?.[1]);
|
|| normalizeNumber(yieldMatch?.[2])
|
||||||
|
|| normalizeNumber(raw.match(/(?:^|\n)\s*KG\s*:\s*([\d.,]+)(?:\s*KG)?/i)?.[1]);
|
||||||
|
const ribKg = normalizeNumber(raw.match(/(?:QUILOS?\s+DE\s+(?:GOLA|RIBANA|VIEZ(?:\s*\([^)]*\))?)|RIBANA)\s*:\s*([\d.,]+)(?:\s*KG)?/i)?.[1]);
|
||||||
|
|
||||||
return { supplier, lotCode, yieldPiecesPerKg, rollQuantity: explicitRolls, fabricKg, ribKg };
|
return { supplier, lotCode, yieldPiecesPerKg, rollQuantity: explicitRolls, fabricKg, ribKg };
|
||||||
};
|
};
|
||||||
@@ -165,6 +171,7 @@ const parseFinalizedProductionOrderCsv = (csv) => {
|
|||||||
|
|
||||||
const refreshYieldRecommendations = async (client) => {
|
const refreshYieldRecommendations = async (client) => {
|
||||||
await client.query('DELETE FROM production_yield_recommendations;');
|
await client.query('DELETE FROM production_yield_recommendations;');
|
||||||
|
await client.query('DELETE FROM production_yield_supplier_recommendations;');
|
||||||
await client.query(`
|
await client.query(`
|
||||||
INSERT INTO production_yield_recommendations (
|
INSERT INTO production_yield_recommendations (
|
||||||
family_key, sample_count, yield_sample_count, roll_sample_count,
|
family_key, sample_count, yield_sample_count, roll_sample_count,
|
||||||
@@ -178,21 +185,44 @@ const refreshYieldRecommendations = async (client) => {
|
|||||||
COUNT(*) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0)::int,
|
COUNT(*) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0)::int,
|
||||||
AVG(yield_pieces_per_kg),
|
AVG(yield_pieces_per_kg),
|
||||||
AVG(fabric_kg / roll_quantity) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0),
|
AVG(fabric_kg / roll_quantity) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0),
|
||||||
AVG(yield_pieces_per_kg) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0 AND yield_pieces_per_kg > 0)
|
AVG(yield_pieces_per_kg * (fabric_kg / roll_quantity)) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0 AND yield_pieces_per_kg > 0),
|
||||||
* AVG(fabric_kg / roll_quantity) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0 AND yield_pieces_per_kg > 0),
|
|
||||||
MAX(completed_date),
|
MAX(completed_date),
|
||||||
CURRENT_TIMESTAMP
|
CURRENT_TIMESTAMP
|
||||||
FROM production_orders
|
FROM production_orders
|
||||||
WHERE status = 'finished'
|
WHERE status = 'finished'
|
||||||
AND split_part(product_sku, '.', 1) IN ('BLCS', 'BLOS', 'BLMC', 'BLPM')
|
AND split_part(product_sku, '.', 1) IN ('BLCS', 'BLOS', 'BLMC', 'BLPM', 'BLRM')
|
||||||
GROUP BY split_part(product_sku, '.', 1);
|
GROUP BY split_part(product_sku, '.', 1);
|
||||||
`);
|
`);
|
||||||
|
await client.query(`
|
||||||
|
INSERT INTO production_yield_supplier_recommendations (
|
||||||
|
family_key, supplier, sample_count, yield_sample_count, roll_sample_count,
|
||||||
|
avg_yield_pieces_per_kg, avg_kg_per_roll, recommended_units_per_roll,
|
||||||
|
last_completed_date, updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
split_part(product_sku, '.', 1) AS family_key,
|
||||||
|
supplier,
|
||||||
|
COUNT(*)::int,
|
||||||
|
COUNT(yield_pieces_per_kg)::int,
|
||||||
|
COUNT(*) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0)::int,
|
||||||
|
AVG(yield_pieces_per_kg),
|
||||||
|
AVG(fabric_kg / roll_quantity) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0),
|
||||||
|
AVG(yield_pieces_per_kg * (fabric_kg / roll_quantity)) FILTER (WHERE fabric_kg > 0 AND roll_quantity > 0 AND yield_pieces_per_kg > 0),
|
||||||
|
MAX(completed_date),
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
FROM production_orders
|
||||||
|
WHERE status = 'finished'
|
||||||
|
AND supplier IS NOT NULL
|
||||||
|
AND split_part(product_sku, '.', 1) IN ('BLCS', 'BLOS', 'BLMC', 'BLPM', 'BLRM')
|
||||||
|
GROUP BY split_part(product_sku, '.', 1), supplier;
|
||||||
|
`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const listProductionOrderImportData = async () => {
|
const listProductionOrderImportData = async () => {
|
||||||
const [runs, recommendations] = await Promise.all([
|
const [runs, recommendations, supplierRecommendations] = await Promise.all([
|
||||||
pool.query(`SELECT id, filename, status, total_rows, imported_count, updated_count, skipped_count, failed_count, error_message, started_at, completed_at FROM production_order_import_runs ORDER BY started_at DESC LIMIT 10;`),
|
pool.query(`SELECT id, filename, status, total_rows, imported_count, updated_count, skipped_count, failed_count, error_message, started_at, completed_at FROM production_order_import_runs ORDER BY started_at DESC LIMIT 10;`),
|
||||||
pool.query(`SELECT family_key, sample_count, yield_sample_count, roll_sample_count, avg_yield_pieces_per_kg, avg_kg_per_roll, recommended_units_per_roll, last_completed_date, updated_at FROM production_yield_recommendations ORDER BY family_key;`)
|
pool.query(`SELECT family_key, sample_count, yield_sample_count, roll_sample_count, avg_yield_pieces_per_kg, avg_kg_per_roll, recommended_units_per_roll, last_completed_date, updated_at FROM production_yield_recommendations ORDER BY family_key;`),
|
||||||
|
pool.query(`SELECT family_key, supplier, sample_count, yield_sample_count, roll_sample_count, avg_yield_pieces_per_kg, avg_kg_per_roll, recommended_units_per_roll, last_completed_date, updated_at FROM production_yield_supplier_recommendations ORDER BY family_key, supplier;`)
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
runs: runs.rows.map(row => ({
|
runs: runs.rows.map(row => ({
|
||||||
@@ -207,6 +237,14 @@ const listProductionOrderImportData = async () => {
|
|||||||
avgKgPerRoll: row.avg_kg_per_roll === null ? null : Number(row.avg_kg_per_roll),
|
avgKgPerRoll: row.avg_kg_per_roll === null ? null : Number(row.avg_kg_per_roll),
|
||||||
recommendedUnitsPerRoll: row.recommended_units_per_roll === null ? null : Number(row.recommended_units_per_roll),
|
recommendedUnitsPerRoll: row.recommended_units_per_roll === null ? null : Number(row.recommended_units_per_roll),
|
||||||
lastCompletedDate: row.last_completed_date || null, updatedAt: row.updated_at
|
lastCompletedDate: row.last_completed_date || null, updatedAt: row.updated_at
|
||||||
|
})),
|
||||||
|
supplierRecommendations: supplierRecommendations.rows.map(row => ({
|
||||||
|
familyKey: row.family_key, supplier: row.supplier, sampleCount: Number(row.sample_count),
|
||||||
|
yieldSampleCount: Number(row.yield_sample_count), rollSampleCount: Number(row.roll_sample_count),
|
||||||
|
avgYieldPiecesPerKg: row.avg_yield_pieces_per_kg === null ? null : Number(row.avg_yield_pieces_per_kg),
|
||||||
|
avgKgPerRoll: row.avg_kg_per_roll === null ? null : Number(row.avg_kg_per_roll),
|
||||||
|
recommendedUnitsPerRoll: row.recommended_units_per_roll === null ? null : Number(row.recommended_units_per_roll),
|
||||||
|
lastCompletedDate: row.last_completed_date || null, updatedAt: row.updated_at
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const {
|
|||||||
test('normalizeFamilyKey accepts only known cut families', () => {
|
test('normalizeFamilyKey accepts only known cut families', () => {
|
||||||
assert.equal(normalizeFamilyKey('blcs'), 'BLCS');
|
assert.equal(normalizeFamilyKey('blcs'), 'BLCS');
|
||||||
assert.equal(normalizeFamilyKey(' BLOS '), 'BLOS');
|
assert.equal(normalizeFamilyKey(' BLOS '), 'BLOS');
|
||||||
|
assert.equal(normalizeFamilyKey('blrm'), 'BLRM');
|
||||||
assert.equal(normalizeFamilyKey('OUTROS'), '');
|
assert.equal(normalizeFamilyKey('OUTROS'), '');
|
||||||
assert.equal(normalizeFamilyKey('unknown'), '');
|
assert.equal(normalizeFamilyKey('unknown'), '');
|
||||||
});
|
});
|
||||||
@@ -21,10 +22,12 @@ test('normalizeFamilyYields keeps positive numeric yield rules', () => {
|
|||||||
BLOS: 0,
|
BLOS: 0,
|
||||||
BLMC: -1,
|
BLMC: -1,
|
||||||
BLPM: '12.5',
|
BLPM: '12.5',
|
||||||
|
BLRM: '17',
|
||||||
OUTROS: 99
|
OUTROS: 99
|
||||||
}), {
|
}), {
|
||||||
BLCS: 50,
|
BLCS: 50,
|
||||||
BLPM: 12.5
|
BLPM: 12.5,
|
||||||
|
BLRM: 17
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,27 @@ test('extractObservationFields reads Brazilian production notes without using th
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('extractObservationFields supports the validated Olist bobbin multiplier, lot, kg and ribana formats', () => {
|
||||||
|
const result = extractObservationFields('FORNECEDOR: ALTUS\nBOBINAS: 07(X2) - 287KG\nRENDIMENTO: 5,55 - 494KG\n- Lote 01\nRIBANA: 3,59');
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
supplier: 'ALTUS',
|
||||||
|
lotCode: '01',
|
||||||
|
yieldPiecesPerKg: 5.55,
|
||||||
|
rollQuantity: 14,
|
||||||
|
fabricKg: 287,
|
||||||
|
ribKg: 3.59
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('extractObservationFields reads standalone KG lines for fabric weight', () => {
|
||||||
|
const result = extractObservationFields('FORNECEDOR: GOL\nBOBINAS: 06\nKG : 204,38\nRENDIMENTO: 5,0\nLote 02');
|
||||||
|
|
||||||
|
assert.equal(result.rollQuantity, 6);
|
||||||
|
assert.equal(result.fabricKg, 204.38);
|
||||||
|
assert.equal(result.lotCode, '02');
|
||||||
|
});
|
||||||
|
|
||||||
test('parseFinalizedProductionOrderCsv supports multiline observations and skips non-finalized OPs', () => {
|
test('parseFinalizedProductionOrderCsv supports multiline observations and skips non-finalized OPs', () => {
|
||||||
const csv = `id_op,numero_op,sku_produto,descricao_produto,quantidade_produzida,data_emissao,data_conclusao,situacao,observacoes,pedido_relacionado\n1,10,BLCS.PTO.G,Camiseta preta G,100,2026-08-01T00:00:00.000Z,2026-08-02T00:00:00.000Z,Finalizada,"FORNECEDOR: NEXSTAR\nRENDIMENTO: 5,44\nQUANTIDADE DE ROLOS: 30\nQUILOS DE MALHA: 577,90",PLANO-1\n2,11,BLCS.PTO.M,Camiseta preta M,100,2026-08-01,2026-08-02,Em andamento,Teste,`;
|
const csv = `id_op,numero_op,sku_produto,descricao_produto,quantidade_produzida,data_emissao,data_conclusao,situacao,observacoes,pedido_relacionado\n1,10,BLCS.PTO.G,Camiseta preta G,100,2026-08-01T00:00:00.000Z,2026-08-02T00:00:00.000Z,Finalizada,"FORNECEDOR: NEXSTAR\nRENDIMENTO: 5,44\nQUANTIDADE DE ROLOS: 30\nQUILOS DE MALHA: 577,90",PLANO-1\n2,11,BLCS.PTO.M,Camiseta preta M,100,2026-08-01,2026-08-02,Em andamento,Teste,`;
|
||||||
const result = parseFinalizedProductionOrderCsv(csv);
|
const result = parseFinalizedProductionOrderCsv(csv);
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ const productionOrder = (overrides: Partial<ProductionOrderItem>): ProductionOrd
|
|||||||
test('classifyCutFamily maps known product bases to internal cut families', () => {
|
test('classifyCutFamily maps known product bases to internal cut families', () => {
|
||||||
assert.equal(classifyCutFamily('BASE LISA CAMISETA').key, 'BLCS');
|
assert.equal(classifyCutFamily('BASE LISA CAMISETA').key, 'BLCS');
|
||||||
assert.equal(classifyCutFamily('BASE LISA CAMISETA OVER').key, 'BLOS');
|
assert.equal(classifyCutFamily('BASE LISA CAMISETA OVER').key, 'BLOS');
|
||||||
assert.equal(classifyCutFamily('BASE LISA MOLETOM CANGURU').key, 'BLPM');
|
assert.equal(classifyCutFamily('BASE LISA MOLETOM CANGURU').key, 'BLMC');
|
||||||
|
assert.equal(classifyCutFamily('BASE LISA CAMISETA PIMA').key, 'BLPM');
|
||||||
|
assert.equal(classifyCutFamily('BASE LISA REGATA MACHAO').key, 'BLRM');
|
||||||
assert.equal(classifyCutFamily('BONÉ').key, 'OUTROS');
|
assert.equal(classifyCutFamily('BONÉ').key, 'OUTROS');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -70,10 +70,10 @@ export interface CutPlan {
|
|||||||
|
|
||||||
export const CUT_FAMILY_RULES: CutFamilyRule[] = [
|
export const CUT_FAMILY_RULES: CutFamilyRule[] = [
|
||||||
{
|
{
|
||||||
key: 'BLPM',
|
key: 'BLMC',
|
||||||
label: 'Moletom',
|
label: 'Moletom canguru',
|
||||||
materialLabel: 'BLPM',
|
materialLabel: 'BLMC',
|
||||||
keywords: ['MOLETOM'],
|
keywords: ['MOLETOM CANGURU', 'MOLETOM'],
|
||||||
unitsPerRoll: null
|
unitsPerRoll: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -84,10 +84,17 @@ export const CUT_FAMILY_RULES: CutFamilyRule[] = [
|
|||||||
unitsPerRoll: null
|
unitsPerRoll: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'BLMC',
|
key: 'BLPM',
|
||||||
label: 'Camiseta infantil',
|
label: 'Camiseta Pima',
|
||||||
materialLabel: 'BLMC',
|
materialLabel: 'BLPM',
|
||||||
keywords: ['INFANTIL', 'KIDS'],
|
keywords: ['CAMISETA PIMA', 'PIMA'],
|
||||||
|
unitsPerRoll: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'BLRM',
|
||||||
|
label: 'Regata',
|
||||||
|
materialLabel: 'BLRM',
|
||||||
|
keywords: ['REGATA'],
|
||||||
unitsPerRoll: null
|
unitsPerRoll: null
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ const issueLabels: Record<CutIssue, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const issueHelp: Record<CutIssue, string> = {
|
const issueHelp: Record<CutIssue, string> = {
|
||||||
missing_family_rule: 'Produto não caiu em BLCS, BLOS, BLMC ou BLPM.',
|
missing_family_rule: 'Produto não caiu em BLCS, BLOS, BLMC, BLPM ou BLRM.',
|
||||||
missing_color: 'Nome do produto não tem uma cor clara para montar matriz de corte.',
|
missing_color: 'Nome do produto não tem uma cor clara para montar matriz de corte.',
|
||||||
missing_size: 'Nome do produto não tem tamanho claro para montar matriz de corte.',
|
missing_size: 'Nome do produto não tem tamanho claro para montar matriz de corte.',
|
||||||
missing_yield_rule: 'Família reconhecida, mas ainda falta cadastrar unidades por rolo.'
|
missing_yield_rule: 'Família reconhecida, mas ainda falta cadastrar unidades por rolo.'
|
||||||
@@ -61,6 +61,7 @@ const familyStyles: Record<CutFamilyKey, string> = {
|
|||||||
BLOS: 'border-sky-400/30 bg-sky-400/10 text-sky-300',
|
BLOS: 'border-sky-400/30 bg-sky-400/10 text-sky-300',
|
||||||
BLMC: 'border-amber-400/30 bg-amber-400/10 text-amber-300',
|
BLMC: 'border-amber-400/30 bg-amber-400/10 text-amber-300',
|
||||||
BLPM: 'border-purple-400/30 bg-purple-400/10 text-purple-300',
|
BLPM: 'border-purple-400/30 bg-purple-400/10 text-purple-300',
|
||||||
|
BLRM: 'border-rose-400/30 bg-rose-400/10 text-rose-300',
|
||||||
OUTROS: 'border-zinc-500/30 bg-zinc-500/10 text-zinc-300'
|
OUTROS: 'border-zinc-500/30 bg-zinc-500/10 text-zinc-300'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1005,7 +1006,7 @@ const Cutting = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{settingsSection === 'rules' && (
|
{settingsSection === 'rules' && (
|
||||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-5">
|
||||||
{CUT_FAMILY_RULES.map(rule => (
|
{CUT_FAMILY_RULES.map(rule => (
|
||||||
<label key={rule.key} className="rounded-xl border border-dark-border bg-dark-input p-3">
|
<label key={rule.key} className="rounded-xl border border-dark-border bg-dark-input p-3">
|
||||||
<span className="mb-2 flex items-center justify-between gap-2">
|
<span className="mb-2 flex items-center justify-between gap-2">
|
||||||
|
|||||||
@@ -38,6 +38,14 @@ const runStatusLabel = (run: OlistSyncRun) => (
|
|||||||
: 'Concluída'
|
: 'Concluída'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const yieldFamilyLabels: Record<string, string> = {
|
||||||
|
BLCS: 'Camiseta regular',
|
||||||
|
BLOS: 'Oversize',
|
||||||
|
BLMC: 'Moletom canguru',
|
||||||
|
BLPM: 'Camiseta Pima',
|
||||||
|
BLRM: 'Regata'
|
||||||
|
};
|
||||||
|
|
||||||
const OlistSync = () => {
|
const OlistSync = () => {
|
||||||
const [status, setStatus] = useState<OlistStatus | null>(null);
|
const [status, setStatus] = useState<OlistStatus | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
@@ -290,16 +298,22 @@ const OlistSync = () => {
|
|||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={() => void loadImportData()} title="Atualizar importações" aria-label="Atualizar importações" className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border text-brand-primary transition-colors hover:border-brand-primary cursor-pointer"><RefreshCw className="h-4 w-4" /></button>
|
<button type="button" onClick={() => void loadImportData()} title="Atualizar importações" aria-label="Atualizar importações" className="inline-flex h-8 w-8 items-center justify-center rounded-lg border border-dark-border text-brand-primary transition-colors hover:border-brand-primary cursor-pointer"><RefreshCw className="h-4 w-4" /></button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-3 p-5 md:grid-cols-4">
|
<div className="grid grid-cols-1 gap-3 p-5 md:grid-cols-5">
|
||||||
{(['BLCS', 'BLOS', 'BLMC', 'BLPM'] as const).map(familyKey => {
|
{(['BLCS', 'BLOS', 'BLMC', 'BLPM', 'BLRM'] as const).map(familyKey => {
|
||||||
const recommendation = importData?.recommendations.find(item => item.familyKey === familyKey);
|
const recommendation = importData?.recommendations.find(item => item.familyKey === familyKey);
|
||||||
return <div key={familyKey} className="rounded-xl border border-dark-border bg-dark-input/50 p-3">
|
return <div key={familyKey} className="rounded-xl border border-dark-border bg-dark-input/50 p-3">
|
||||||
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">{familyKey}</p>
|
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">{familyKey} · {yieldFamilyLabels[familyKey]}</p>
|
||||||
<p className="mt-2 text-lg font-bold text-dark-text">{recommendation?.recommendedUnitsPerRoll ? `${Math.round(recommendation.recommendedUnitsPerRoll).toLocaleString('pt-BR')} un./rolo` : 'Sem sugestão'}</p>
|
<p className="mt-2 text-lg font-bold text-dark-text">{recommendation?.recommendedUnitsPerRoll ? `${Math.round(recommendation.recommendedUnitsPerRoll).toLocaleString('pt-BR')} un./rolo` : 'Sem sugestão'}</p>
|
||||||
<p className="mt-1 text-xs font-semibold text-dark-muted">{recommendation ? `${recommendation.sampleCount.toLocaleString('pt-BR')} OPs · ${recommendation.yieldSampleCount.toLocaleString('pt-BR')} c/ rendimento` : 'Nenhuma OP importada'}</p>
|
<p className="mt-1 text-xs font-semibold text-dark-muted">{recommendation ? `${recommendation.sampleCount.toLocaleString('pt-BR')} OPs · ${recommendation.yieldSampleCount.toLocaleString('pt-BR')} c/ rendimento` : 'Nenhuma OP importada'}</p>
|
||||||
</div>;
|
</div>;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
{!!importData?.supplierRecommendations.length && <div className="border-t border-dark-border px-5 pb-5 pt-4">
|
||||||
|
<p className="text-[10px] font-bold uppercase tracking-widest text-dark-muted">Referências por fornecedor</p>
|
||||||
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
|
{importData.supplierRecommendations.filter(item => item.recommendedUnitsPerRoll).map(item => <span key={`${item.familyKey}-${item.supplier}`} className="rounded-full border border-brand-primary/30 bg-brand-primary/10 px-2.5 py-1 text-xs font-bold text-brand-primary">{item.familyKey} · {item.supplier}: {Math.round(item.recommendedUnitsPerRoll!).toLocaleString('pt-BR')} un./rolo</span>)}
|
||||||
|
</div>
|
||||||
|
</div>}
|
||||||
<div className="divide-y divide-dark-border">
|
<div className="divide-y divide-dark-border">
|
||||||
{!importData?.runs.length && <p className="p-5 text-sm font-semibold text-dark-muted">Nenhum CSV de OPs finalizadas importado.</p>}
|
{!importData?.runs.length && <p className="p-5 text-sm font-semibold text-dark-muted">Nenhum CSV de OPs finalizadas importado.</p>}
|
||||||
{importData?.runs.map(run => <div key={run.id} className="flex flex-col gap-2 p-4 md:flex-row md:items-center md:justify-between">
|
{importData?.runs.map(run => <div key={run.id} className="flex flex-col gap-2 p-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export interface CreateProductionOrdersResult {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CutFamilyKey = 'BLCS' | 'BLOS' | 'BLMC' | 'BLPM' | 'OUTROS';
|
export type CutFamilyKey = 'BLCS' | 'BLOS' | 'BLMC' | 'BLPM' | 'BLRM' | 'OUTROS';
|
||||||
|
|
||||||
export interface CutProductOverride {
|
export interface CutProductOverride {
|
||||||
familyKey?: CutFamilyKey | '';
|
familyKey?: CutFamilyKey | '';
|
||||||
@@ -272,7 +272,7 @@ export interface ProductionOrderImportRun {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductionYieldRecommendation {
|
export interface ProductionYieldRecommendation {
|
||||||
familyKey: 'BLCS' | 'BLOS' | 'BLMC' | 'BLPM';
|
familyKey: 'BLCS' | 'BLOS' | 'BLMC' | 'BLPM' | 'BLRM';
|
||||||
sampleCount: number;
|
sampleCount: number;
|
||||||
yieldSampleCount: number;
|
yieldSampleCount: number;
|
||||||
rollSampleCount: number;
|
rollSampleCount: number;
|
||||||
@@ -283,9 +283,14 @@ export interface ProductionYieldRecommendation {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProductionYieldSupplierRecommendation extends ProductionYieldRecommendation {
|
||||||
|
supplier: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProductionOrderImportData {
|
export interface ProductionOrderImportData {
|
||||||
runs: ProductionOrderImportRun[];
|
runs: ProductionOrderImportRun[];
|
||||||
recommendations: ProductionYieldRecommendation[];
|
recommendations: ProductionYieldRecommendation[];
|
||||||
|
supplierRecommendations: ProductionYieldSupplierRecommendation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CatalogProductPayload = {
|
export type CatalogProductPayload = {
|
||||||
|
|||||||
Reference in New Issue
Block a user