/* ===== Excel upload modal — column mapping + row validation ===== */
const ExcelUploadModal = ({ open, onClose, onConfirm }) => {
const [step, setStep] = React.useState(2); // skip to mapping for demo
if (!open) return null;
return (
e.stopPropagation()}>
{/* header */}
Import cargo manifest
shanghai_export_w19.xlsx · 3 sheets · 142 rows
{/* body */}
{step === 0 && setStep(1)} />}
{step === 1 && setStep(2)} />}
{step === 2 && setStep(3)} />}
{step === 3 && { onConfirm(); }} />}
{/* footer */}
{step < 3 ? (
) : (
)}
);
};
const Stepper = ({ step, steps }) => (
{steps.map((s, i) => (
{i : null}
{s}
{i›}
))}
);
const UploadDrop = ({ onNext }) => (
Drop your file here or browse
.xlsx, .xls, .csv up to 25 MB
);
const SheetPicker = ({ onNext }) => (
Detected sheets
{['cargo_manifest','rules','category_matrix'].map((n, i) => (
))}
);
const ColumnMapper = ({ onNext }) => {
const COLS = [
{ their: 'SKU_CODE', ours: 'sku', conf: 0.99, sample: 'GD-1029' },
{ their: 'Description', ours: 'desc', conf: 0.96, sample: 'Loose box · Tools' },
{ their: 'Lenght (mm)', ours: 'L', conf: 0.92, sample: '380' },
{ their: 'Width-mm', ours: 'W', conf: 0.94, sample: '240' },
{ their: 'Hght', ours: 'H', conf: 0.78, sample: '220' },
{ their: 'WGT (kg)', ours: 'weight', conf: 0.95, sample: '11' },
{ their: 'QTY', ours: 'qty', conf: 0.99, sample: '6' },
{ their: 'Cat.', ours: 'category', conf: 0.71, sample: 'general' },
{ their: 'STK', ours: 'stackable', conf: 0.42, sample: 'Y' },
{ their: 'STOP_NO', ours: 'stop', conf: 0.88, sample: '2' },
{ their: 'orient', ours: 'orientation', conf: 0.86, sample: 'upright' },
];
return (
Column mapping
We auto-matched 11 of 11 columns. Review and confirm.
| Your column |
Sample |
Mojro field |
Confidence |
{COLS.map(c => {
const tone = c.conf >= 0.9 ? 'ok' : c.conf >= 0.7 ? 'warn' : 'bad';
return (
| {c.their} |
{c.sample} |
|
{Math.round(c.conf*100)}%
|
);
})}
);
};
const RowValidator = ({ onConfirm }) => {
const ROWS = [
{ row: 1, status:'ok', sku:'GD-1029', qty: 6, L: 380, W: 240, H: 220, weight:11, err: null },
{ row: 2, status:'ok', sku:'BX-LRG-01',qty:14, L: 600, W: 400, H: 400, weight:22, err: null },
{ row: 3, status:'warn', sku:'PP-3M-DN50',qty:12, L: 3000,W: 60, H: 60, weight:18, err: 'Length 3000mm exceeds preferred carton; will fit container only.' },
{ row: 4, status:'ok', sku:'BR-200L', qty: 4, L: 580, W: 580, H: 880, weight:196, err: null },
{ row: 5, status:'bad', sku:'BX-?', qty: '?',L: '?', W: 400, H: 280, weight:14, err: 'Missing required: sku, qty, L. Skip row or fix.' },
{ row: 6, status:'ok', sku:'PLT-EUR-A',qty: 8, L:1200, W: 800, H:1450, weight:720, err: null },
{ row: 7, status:'warn', sku:'PLT-EUR-C',qty: 4, L:1200, W: 800, H:1300, weight:540, err: 'Category "Hazmat" requires segregation distance — applied automatically.' },
];
const counts = ROWS.reduce((a, r) => (a[r.status]++, a), { ok:0, warn:0, bad:0 });
return (
Row validation
{ROWS.length} rows scanned in cargo_manifest
{counts.ok} valid
{counts.warn} warnings
{counts.bad} blocking
| Row |
Status |
SKU |
Qty |
L×W×H (mm) |
Weight (kg) |
Notes |
{ROWS.map(r => (
| {r.row} |
{r.status==='ok' && OK}
{r.status==='warn' && warn}
{r.status==='bad' && error}
|
{r.sku} |
{r.qty} |
{r.L}×{r.W}×{r.H} |
{r.weight} |
{r.err || —} |
))}
Importing will skip 1 blocking row and apply 2 auto-corrections. You can fix and re-import the skipped row at any time.
);
};
window.ExcelUploadModal = ExcelUploadModal;