/* ═══════════════════════════════════════════════════════════ Domain model — constants, key helpers, reducer, utilities. Logic is preserved verbatim from the original app; only the module wiring (window exports) and badge styling changed. ═══════════════════════════════════════════════════════════ */ // ── CONSTANTS ──────────────────────────────────────────────── const GUNS = ['1א', '1ב', '1ג', '2א', '2ב', '2ג']; const CHARGE_BEHAVIORS = { 'M7 אחוד': { kind: 'standard', unit: 'יחידות' }, 'M7 פרוד': { kind: 'separate', unit: 'יחידות', subOpts: ['M7/4', 'M7/5', 'M7/6', 'M7/7'] }, 'M5 אחוד': { kind: 'standard', unit: 'יחידות' }, 'M8.5': { kind: 'standard', unit: 'יחידות' }, 'M9': { kind: 'separate', unit: 'יחידות', subOpts: ['M9/6', 'M9/7', 'M9/8', 'M9/9'] }, 'M10': { kind: 'standard', unit: 'יחידות' }, 'M231': { kind: 'modular', unit: 'תתי מטענים', subOpts: ['M231/1', 'M231/2'] }, }; const CHARGE_TYPE_NAMES = Object.keys(CHARGE_BEHAVIORS); // ── KEY HELPERS ────────────────────────────────────────────── const makeShellKey = n => 'shell::' + n.trim(); const makeFuzeKey = n => 'fuze::' + n.trim(); const makeChargeKey = (t, s) => 'charge::' + t + '::' + s.trim(); function parseKey(key) { if (!key) return null; const [cat, ...rest] = key.split('::'); if (cat === 'shell') return { category: 'shell', name: rest[0] }; if (cat === 'fuze') return { category: 'fuze', name: rest[0] }; if (cat === 'charge') return { category: 'charge', chargeType: rest[0], series: rest[1] }; return null; } function keyLabel(key, regs) { if (!key) return ''; const r = (regs || []).find(x => x.key === key); if (r) return r.label; const p = parseKey(key); if (!p) return key; if (p.category === 'shell') return 'פגז ' + p.name; if (p.category === 'fuze') return 'מרעום ' + p.name; if (p.category === 'charge') return 'חנ"ה ' + p.chargeType + ' סדרה ' + p.series; return key; } function keyUnit(key) { const p = parseKey(key); if (!p) return ''; if (p.category === 'shell') return 'פגזים'; if (p.category === 'fuze') return 'מרעומים'; if (p.category === 'charge') return (CHARGE_BEHAVIORS[p.chargeType] || {}).unit || 'יחידות'; return ''; } function makeTypeRecord(key) { const p = parseKey(key); if (!p) return null; if (p.category === 'shell') return { key, category: 'shell', label: 'פגז ' + p.name, unit: 'פגזים' }; if (p.category === 'fuze') return { key, category: 'fuze', label: 'מרעום ' + p.name, unit: 'מרעומים' }; if (p.category === 'charge') { const b = CHARGE_BEHAVIORS[p.chargeType] || {}; return { key, category: 'charge', chargeType: p.chargeType, series: p.series, label: 'חנ"ה ' + p.chargeType + ' סדרה ' + p.series + (b.kind === 'modular' ? ' (תתי מטענים)' : ''), unit: b.unit || 'יחידות', }; } return null; } function chargeInvDeduction(chargeType, subOpt, shellQty) { const b = CHARGE_BEHAVIORS[chargeType]; if (!b || !shellQty) return shellQty || 0; if (b.kind === 'modular') return shellQty * (subOpt === 'M231/2' ? 2 : 1); return shellQty; } // ── STATE / REDUCER ────────────────────────────────────────── const LS_KEY = 'arty_ammo_v6'; const M85_DEFAULT = { key: 'charge::M8.5::', label: 'חנ"ה M8.5', category: 'charge', unit: 'יחידות', chargeType: 'M8.5', series: '' }; function buildInit() { const guns = {}; GUNS.forEach(g => { guns[g] = {}; }); const barrelData = {}; GUNS.forEach(g => { barrelData[g] = { lastCleanTs: null, shellsSinceClean: 0, totalShells: 0, muzzle: { count: 0, lastResetTs: null }, barrel: { count: 0, lastResetTs: null }, m10Count: 0 }; }); return { guns, bunker: [], nextPalletId: 1, nextMissionSeq: 1, registeredTypes: [M85_DEFAULT], ammoGroups: [], // user-defined groups: [{ id, label, keys[], redLine }] firingLog: [], transferLog: [], quarantineLog: [], lastFireMissionId: null, lastUpdateTs: null, redLines: {}, barrelData, readiness: { red: '', yellow: '' }, }; } function applyMigrations(st) { if (!st) return buildInit(); // Work on a shallow copy — never mutate the passed-in object st = { ...st }; if (!st.registeredTypes) st.registeredTypes = [M85_DEFAULT]; if (!st.registeredTypes.find(r => r.key === M85_DEFAULT.key)) { st.registeredTypes = [M85_DEFAULT, ...st.registeredTypes]; } if (!st.barrelData) st.barrelData = {}; st.barrelData = { ...st.barrelData }; GUNS.forEach(g => { if (!st.barrelData[g]) st.barrelData[g] = { lastCleanTs: null, shellsSinceClean: 0, totalShells: 0, muzzle: { count: 0, lastResetTs: null }, barrel: { count: 0, lastResetTs: null }, m10Count: 0 }; else st.barrelData[g] = { ...st.barrelData[g] }; const gd = st.barrelData[g]; if (!gd.muzzle) gd.muzzle = { count: 0, lastResetTs: null }; if (!gd.barrel) gd.barrel = { count: 0, lastResetTs: null }; if (gd.m10Count === undefined) gd.m10Count = 0; }); if (!st.guns) st.guns = {}; GUNS.forEach(g => { if (!st.guns[g]) st.guns = { ...st.guns, [g]: {} }; }); if (!st.bunker) st.bunker = []; if (!st.firingLog) st.firingLog = []; if (!st.transferLog) st.transferLog = []; if (!st.quarantineLog) st.quarantineLog = []; if (!st.readiness) st.readiness = { red: '', yellow: '' }; if (!st.redLines) st.redLines = {}; if (!st.ammoGroups) st.ammoGroups = []; return st; } function saveState(s) { // No localStorage — state lives exclusively in DynamoDB and React memory. if (window.api) window.api.saveState(s); } function mergeRegs(existing, incoming) { const seen = new Set(existing.map(r => r.key)); return [...existing, ...incoming.filter(r => !seen.has(r.key))]; } function reducer(state, action) { const ts = new Date().toISOString(); switch (action.type) { case 'ADD_PALLET': { const id = state.nextPalletId; const logItems = Object.entries(action.ammo).filter(([, v]) => v > 0).map(([k, v]) => ({ key: k, qty: v })); return { ...state, lastUpdateTs: ts, bunker: [...state.bunker, { id, ammo: { ...action.ammo } }], nextPalletId: id + 1, registeredTypes: mergeRegs(state.registeredTypes, action.newTypes || []), transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: 'קליטה חיצונית', dst: 'בונקר משטח #' + id, items: logItems }], }; } case 'CLEAR_PALLET': return { ...state, lastUpdateTs: ts, bunker: state.bunker.filter(p => p.id !== action.palletId) }; case 'TRANSFER_PALLET_TO_GUN': { const { palletId, gun, items } = action; const bunker = state.bunker.map(p => { if (p.id !== palletId) return p; const ammo = { ...p.ammo }; items.forEach(({ key, qty }) => { ammo[key] = Math.max(0, (ammo[key] || 0) - qty); }); return { ...p, ammo }; }); const guns = { ...state.guns, [gun]: { ...state.guns[gun] } }; items.forEach(({ key, qty }) => { guns[gun][key] = (guns[gun][key] || 0) + qty; }); return { ...state, lastUpdateTs: ts, bunker, guns, transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: 'בונקר משטח #' + palletId, dst: 'צוות ' + gun, items }] }; } case 'TRANSFER_GUN_TO_PALLET': { const { gun, palletId, items } = action; const bunker = state.bunker.map(p => { if (p.id !== palletId) return p; const ammo = { ...p.ammo }; items.forEach(({ key, qty }) => { ammo[key] = (ammo[key] || 0) + qty; }); return { ...p, ammo }; }); const guns = { ...state.guns, [gun]: { ...state.guns[gun] } }; items.forEach(({ key, qty }) => { guns[gun][key] = Math.max(0, (guns[gun][key] || 0) - qty); }); return { ...state, lastUpdateTs: ts, bunker, guns, transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: 'צוות ' + gun, dst: 'בונקר משטח #' + palletId, items }] }; } case 'TRANSFER_GUN_TO_GUN': { const { srcGun, dstGun, items } = action; const guns = { ...state.guns, [srcGun]: { ...state.guns[srcGun] }, [dstGun]: { ...state.guns[dstGun] } }; items.forEach(({ key, qty }) => { guns[srcGun][key] = Math.max(0, (guns[srcGun][key] || 0) - qty); guns[dstGun][key] = (guns[dstGun][key] || 0) + qty; }); return { ...state, lastUpdateTs: ts, guns, transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: 'צוות ' + srcGun, dst: 'צוות ' + dstGun, items }] }; } case 'FIRE_MISSION': { const guns = { ...state.guns }; const barrelData = JSON.parse(JSON.stringify(state.barrelData || {})); GUNS.forEach(g => { if (!barrelData[g]) barrelData[g] = { lastCleanTs: null, shellsSinceClean: 0, totalShells: 0, muzzle: { count: 0, lastResetTs: null }, barrel: { count: 0, lastResetTs: null }, m10Count: 0 }; }); action.entries.forEach(({ gun, ammoItems }) => { guns[gun] = { ...guns[gun] }; ammoItems.forEach(({ key, invDeduction }) => { guns[gun][key] = Math.max(0, (guns[gun][key] || 0) - invDeduction); }); const shellsFired = ammoItems.filter(i => parseKey(i.key)?.category === 'shell').reduce((s, i) => s + i.qty, 0); if (shellsFired > 0) { barrelData[gun].shellsSinceClean = (barrelData[gun].shellsSinceClean || 0) + shellsFired; barrelData[gun].totalShells = (barrelData[gun].totalShells || 0) + shellsFired; } const m10Fired = ammoItems.filter(i => { const p = parseKey(i.key); return p?.category === 'charge' && p.chargeType === 'M10'; }).reduce((s, i) => s + i.qty, 0); if (m10Fired > 0) { if (barrelData[gun].m10Count === undefined) barrelData[gun].m10Count = 0; barrelData[gun].m10Count = (barrelData[gun].m10Count || 0) + m10Fired; if (!barrelData[gun].muzzle) barrelData[gun].muzzle = { count: 0, lastResetTs: null }; if (!barrelData[gun].barrel) barrelData[gun].barrel = { count: 0, lastResetTs: null }; barrelData[gun].muzzle.count = (barrelData[gun].muzzle.count || 0) + m10Fired; barrelData[gun].barrel.count = (barrelData[gun].barrel.count || 0) + m10Fired; } }); const newRows = action.entries.map(({ gun, ammoItems, combos }) => ({ id: genId(), date: action.meta.date, time: action.meta.time, unit: action.meta.unit, target: action.meta.target, missionId: action.meta.missionId, mapiq: action.meta.mapiq || '', notes: action.meta.notes || '', gun, ammoItems, combos: combos || [], })); const sortedLogFM = [...state.firingLog, ...newRows].sort((a, b) => { const da = new Date((a.date || '1970-01-01') + 'T' + (a.time || '00:00')); const db = new Date((b.date || '1970-01-01') + 'T' + (b.time || '00:00')); return db - da; }); return { ...state, lastUpdateTs: ts, guns, barrelData, lastFireMissionId: action.meta.missionId, nextMissionSeq: (state.nextMissionSeq || 1) + 1, firingLog: sortedLogFM }; } case 'EDIT_MISSION_ROW': { const { rowId, oldAmmoItems, newAmmoItems, newCombos, gun } = action; const guns = { ...state.guns, [gun]: { ...state.guns[gun] } }; oldAmmoItems.forEach(({ key, invDeduction }) => { guns[gun][key] = (guns[gun][key] || 0) + invDeduction; }); newAmmoItems.forEach(({ key, invDeduction }) => { guns[gun][key] = Math.max(0, (guns[gun][key] || 0) - invDeduction); }); const updatedLog = state.firingLog.map(r => { if (r.id !== rowId) return r; return { ...r, ammoItems: newAmmoItems, combos: newCombos || r.combos, notes: action.newNotes !== undefined ? action.newNotes : r.notes, date: action.newDate || r.date, time: action.newTime || r.time }; }); const firingLog = [...updatedLog].sort((a, b) => { const da = new Date((a.date || '1970-01-01') + 'T' + (a.time || '00:00')); const db = new Date((b.date || '1970-01-01') + 'T' + (b.time || '00:00')); return db - da; }); return { ...state, lastUpdateTs: ts, guns, firingLog }; } case 'IMPORT_STATE': return { ...buildInit(), ...action.payload, lastUpdateTs: new Date().toISOString() }; case 'SET_RED_LINE': return { ...state, redLines: { ...(state.redLines || {}), [action.key]: action.value } }; case 'RESET_BARREL': { const barrelData = JSON.parse(JSON.stringify(state.barrelData || {})); if (!barrelData[action.gun]) barrelData[action.gun] = { lastCleanTs: null, shellsSinceClean: 0, totalShells: 0, muzzle: { count: 0, lastResetTs: null }, barrel: { count: 0, lastResetTs: null }, m10Count: 0 }; barrelData[action.gun].shellsSinceClean = 0; barrelData[action.gun].lastCleanTs = ts; return { ...state, lastUpdateTs: ts, barrelData }; } case 'RESET_MUZZLE': { const barrelData = JSON.parse(JSON.stringify(state.barrelData || {})); if (!barrelData[action.gun]) barrelData[action.gun] = { lastCleanTs: null, shellsSinceClean: 0, totalShells: 0, muzzle: { count: 0, lastResetTs: null }, barrel: { count: 0, lastResetTs: null }, m10Count: 0 }; if (!barrelData[action.gun].muzzle) barrelData[action.gun].muzzle = { count: 0, lastResetTs: null }; barrelData[action.gun].muzzle = { count: 0, lastResetTs: ts }; return { ...state, lastUpdateTs: ts, barrelData }; } case 'RESET_BARREL_WEAR': { const barrelData = JSON.parse(JSON.stringify(state.barrelData || {})); if (!barrelData[action.gun]) barrelData[action.gun] = { lastCleanTs: null, shellsSinceClean: 0, totalShells: 0, muzzle: { count: 0, lastResetTs: null }, barrel: { count: 0, lastResetTs: null }, m10Count: 0 }; if (!barrelData[action.gun].barrel) barrelData[action.gun].barrel = { count: 0, lastResetTs: null }; barrelData[action.gun].barrel = { count: 0, lastResetTs: ts }; return { ...state, lastUpdateTs: ts, barrelData }; } case 'SET_READINESS': return { ...state, readiness: { ...(state.readiness || {}), [action.level]: action.gun } }; case 'EDIT_TRANSFER': { const { rowId, oldItems, newItems, src, dst } = action; function parseEnt(str) { if (!str) return { type: 'external' }; const pm = str.match(/משטח #(\d+)/); if (pm) return { type: 'bunker', id: parseInt(pm[1]) }; const gm = str.match(/צוות (.+)/); if (gm) return { type: 'gun', gun: gm[1] }; return { type: 'external' }; } const srcEnt = parseEnt(src), dstEnt = parseEnt(dst); const oldMap = {}, newMap = {}; (oldItems || []).forEach(i => { oldMap[i.key] = (oldMap[i.key] || 0) + i.qty; }); (newItems || []).forEach(i => { newMap[i.key] = (newMap[i.key] || 0) + i.qty; }); const allKeys = [...new Set([...Object.keys(oldMap), ...Object.keys(newMap)])]; const guns = { ...state.guns }; let bunker = [...state.bunker]; function applyEnt(ent, key, delta) { if (ent.type === 'external') return; if (ent.type === 'gun') { if (!guns[ent.gun]) return; guns[ent.gun] = { ...guns[ent.gun] }; guns[ent.gun][key] = Math.max(0, (guns[ent.gun][key] || 0) + delta); } if (ent.type === 'bunker') { bunker = bunker.map(p => { if (p.id !== ent.id) return p; const ammo = { ...p.ammo }; ammo[key] = Math.max(0, (ammo[key] || 0) + delta); return { ...p, ammo }; }); } } allKeys.forEach(key => { const oldQ = oldMap[key] || 0, newQ = newMap[key] || 0; const delta = oldQ - newQ; applyEnt(srcEnt, key, delta); applyEnt(dstEnt, key, -delta); }); const transferLog = state.transferLog.map(r => r.id === rowId ? { ...r, items: newItems } : r); return { ...state, lastUpdateTs: ts, guns, bunker, transferLog }; } case 'GROUND_INTAKE': { const { gun, ammo, newTypes } = action; const guns = { ...state.guns, [gun]: { ...ammo } }; const logItems = Object.entries(ammo).map(([k, v]) => ({ key: k, qty: v })); return { ...state, lastUpdateTs: ts, guns, registeredTypes: mergeRegs(state.registeredTypes, newTypes || []), transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: 'מלאי פתיחה / ערום קרקע', dst: 'צוות ' + gun, items: logItems }] }; } case 'EDIT_GUN_STOCK': { const { gun, ammo, newTypes } = action; const guns = { ...state.guns, [gun]: { ...ammo } }; const logItems = Object.entries(ammo).map(([k, v]) => ({ key: k, qty: v })); return { ...state, lastUpdateTs: ts, guns, registeredTypes: mergeRegs(state.registeredTypes, newTypes || []), transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: 'עדכון ידני', dst: `עדכון ועריכת רכיבי ערום לצוות ${gun}`, items: logItems }] }; } case 'ADD_TO_QUARANTINE': { const { srcType, gunId, palletId, items, reason, approver, imageB64 } = action; const guns = { ...state.guns }; let bunker = [...state.bunker]; const srcLabel = srcType === 'gun' ? 'צוות ' + gunId : 'בונקר משטח #' + palletId; if (srcType === 'gun') { guns[gunId] = { ...guns[gunId] }; items.forEach(({ key, qty }) => { guns[gunId][key] = Math.max(0, (guns[gunId][key] || 0) - qty); }); } else { bunker = bunker.map(p => { if (p.id !== palletId) return p; const ammo = { ...p.ammo }; items.forEach(({ key, qty }) => { ammo[key] = Math.max(0, (ammo[key] || 0) - qty); }); return { ...p, ammo }; }); } const qEntry = { id: genId(), datetime: ts, src: srcLabel, items, reason, approver, imageB64: imageB64 || null }; return { ...state, lastUpdateTs: ts, guns, bunker, quarantineLog: [...(state.quarantineLog || []), qEntry], transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: srcLabel, dst: '⛔ פינת פסולים', items }] }; } case 'RETURN_FROM_QUARANTINE': { const { entryId, dstGun } = action; const entry = (state.quarantineLog || []).find(e => e.id === entryId); if (!entry) return state; const quarantineLog = (state.quarantineLog || []).filter(e => e.id !== entryId); const guns = { ...state.guns, [dstGun]: { ...state.guns[dstGun] } }; (entry.items || []).forEach(({ key, qty }) => { guns[dstGun][key] = (guns[dstGun][key] || 0) + qty; }); return { ...state, lastUpdateTs: ts, guns, quarantineLog, transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: '⛔ פינת פסולים (החזרה לשימוש)', dst: 'צוות ' + dstGun, items: entry.items || [] }] }; } case 'SCRAP_QUARANTINE': { const { entryId } = action; const entry = (state.quarantineLog || []).find(e => e.id === entryId); if (!entry) return state; const quarantineLog = (state.quarantineLog || []).filter(e => e.id !== entryId); return { ...state, lastUpdateTs: ts, quarantineLog, transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: '⛔ פינת פסולים', dst: '🗑 גריטת תחמושת (סילוק סופי)', items: entry.items || [] }] }; } case 'EDIT_PALLET_STOCK': { const { palletId, ammo } = action; const bunker = state.bunker.map(p => p.id !== palletId ? p : { ...p, ammo: { ...ammo } }); const logItems = Object.entries(ammo).filter(([, v]) => v > 0).map(([k, v]) => ({ key: k, qty: v })); return { ...state, lastUpdateTs: ts, bunker, transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: 'עדכון ידני', dst: `עדכון מלאי משטח #${palletId}`, items: logItems }] }; } case 'EDIT_MISSION_TIMESTAMP': { const { rowId, newDate, newTime } = action; const updatedLogTS = state.firingLog.map(r => r.id === rowId ? { ...r, date: newDate, time: newTime } : r); const firingLog = [...updatedLogTS].sort((a, b) => { const da = new Date((a.date || '1970-01-01') + 'T' + (a.time || '00:00')); const db = new Date((b.date || '1970-01-01') + 'T' + (b.time || '00:00')); return db - da; }); return { ...state, lastUpdateTs: ts, firingLog }; } // ── Central catalog (Settings page) ────────────────────── // The single source of truth for פגז / חנ"ה / מרעום naming. case 'ADD_REGISTERED_TYPE': { if (!action.record || state.registeredTypes.find(r => r.key === action.record.key)) return state; return { ...state, lastUpdateTs: ts, registeredTypes: [...state.registeredTypes, action.record] }; } case 'UPDATE_REGISTERED_TYPE': { return { ...state, lastUpdateTs: ts, registeredTypes: state.registeredTypes.map(r => r.key === action.key ? { ...r, label: action.label ?? r.label, unit: action.unit ?? r.unit } : r) }; } case 'DELETE_REGISTERED_TYPE': { if (action.key === M85_DEFAULT.key) return state; // protected base entry return { ...state, lastUpdateTs: ts, registeredTypes: state.registeredTypes.filter(r => r.key !== action.key) }; } // ── AMMO GROUPS ─────────────────────────────────────────── // Groups are user-defined collections of ammo keys with a shared // red line on the combined total (on-ground + רמסע). case 'ADD_AMMO_GROUP': { const group = { id: genId(), label: action.label, keys: action.keys || [], redLine: action.redLine || 0 }; return { ...state, lastUpdateTs: ts, ammoGroups: [...(state.ammoGroups || []), group] }; } case 'EDIT_AMMO_GROUP': { return { ...state, lastUpdateTs: ts, ammoGroups: (state.ammoGroups || []).map(g => g.id !== action.id ? g : { ...g, label: action.label ?? g.label, keys: action.keys ?? g.keys, redLine: action.redLine ?? g.redLine } )}; } case 'DELETE_AMMO_GROUP': { return { ...state, lastUpdateTs: ts, ammoGroups: (state.ammoGroups || []).filter(g => g.id !== action.id) }; } case 'RESET_STATE': return buildInit(); // ── DELETE_MISSION ──────────────────────────────────────── case 'DELETE_MISSION': { const row = (state.firingLog || []).find(r => r.id === action.rowId); if (!row) return state; const guns = { ...state.guns, [row.gun]: { ...state.guns[row.gun] } }; (row.ammoItems || []).forEach(({ key, invDeduction }) => { guns[row.gun][key] = (guns[row.gun][key] || 0) + (invDeduction || 0); }); const firingLog = state.firingLog.filter(r => r.id !== action.rowId); return { ...state, lastUpdateTs: ts, guns, firingLog, lastFireMissionId: firingLog.length ? firingLog[0].missionId : null }; } // ── FORCE_DELETE_TYPE ───────────────────────────────────── case 'FORCE_DELETE_TYPE': { if (action.key === M85_DEFAULT.key) return state; const guns = { ...state.guns }; GUNS.forEach(g => { if (guns[g] && guns[g][action.key] !== undefined) { guns[g] = { ...guns[g] }; delete guns[g][action.key]; } }); const bunker = state.bunker.map(p => { if (p.ammo[action.key] === undefined) return p; const ammo = { ...p.ammo }; delete ammo[action.key]; return { ...p, ammo }; }); return { ...state, lastUpdateTs: ts, guns, bunker, registeredTypes: state.registeredTypes.filter(r => r.key !== action.key) }; } // ── REMAP_AMMO_TYPE ─────────────────────────────────────── case 'REMAP_AMMO_TYPE': { const { srcKey, dstKey } = action; if (srcKey === M85_DEFAULT.key || srcKey === dstKey) return state; const guns = { ...state.guns }; GUNS.forEach(g => { const srcQty = guns[g]?.[srcKey] || 0; if (!srcQty && guns[g]?.[srcKey] === undefined) return; guns[g] = { ...guns[g] }; if (srcQty > 0) guns[g][dstKey] = (guns[g][dstKey] || 0) + srcQty; delete guns[g][srcKey]; }); const bunker = state.bunker.map(p => { const srcQty = p.ammo[srcKey] || 0; if (!srcQty && p.ammo[srcKey] === undefined) return p; const ammo = { ...p.ammo }; if (srcQty > 0) ammo[dstKey] = (ammo[dstKey] || 0) + srcQty; delete ammo[srcKey]; return { ...p, ammo }; }); return { ...state, lastUpdateTs: ts, guns, bunker, registeredTypes: state.registeredTypes.filter(r => r.key !== srcKey) }; } // ── RESET_GUN_INVENTORY ─────────────────────────────────── case 'RESET_GUN_INVENTORY': { const guns = { ...state.guns, [action.gun]: {} }; return { ...state, lastUpdateTs: ts, guns, transferLog: [...state.transferLog, { id: genId(), datetime: ts, src: `צוות ${action.gun}`, dst: '↺ איפוס מלאי ידני', items: Object.entries(state.guns[action.gun] || {}).map(([k, v]) => ({ key: k, qty: v })) }] }; } default: return state; } } // ── UTILS ──────────────────────────────────────────────────── const genId = () => Math.random().toString(36).slice(2) + Date.now().toString(36); function genMissionId(date, seq, mapiq) { const s = String(seq || 1).padStart(2, '0'); const d = date ? new Date(date + 'T00:00:00') : new Date(); const dd = String(d.getDate()).padStart(2, '0'); const mm = String(d.getMonth() + 1).padStart(2, '0'); const yy = String(d.getFullYear()).slice(2); const nm = (mapiq || '').trim(); return '#' + s + '-' + dd + '/' + mm + '/' + yy + (nm ? '-' + nm : ''); } function fmtDatetime(iso) { if (!iso) return '—'; const d = new Date(iso); return d.toLocaleDateString('he-IL') + ' ' + d.toLocaleTimeString('he-IL', { hour: '2-digit', minute: '2-digit' }); } function ammoQtyBadge(qty) { // Plain-JS module (loaded before Babel) — build the node without JSX. // red = 0 (depleted), orange = 1-10 (low), green = >10 (available) const h = React.createElement; if (qty === 0) return h('span', { className: 'badge badge-red', title: 'אזל מהמלאי' }, '0'); if (qty <= 10) return h('span', { className: 'badge badge-orange' }, qty); return h('span', { className: 'badge badge-green' }, qty); } function totalAmmo(state) { const t = {}; (state.registeredTypes || []).forEach(r => { t[r.key] = 0; }); GUNS.forEach(g => Object.entries(state.guns[g] || {}).forEach(([k, v]) => { t[k] = (t[k] || 0) + v; })); (state.bunker || []).forEach(p => Object.entries(p.ammo || {}).forEach(([k, v]) => { t[k] = (t[k] || 0) + v; })); return t; } // Guns only (on-ground / ערום קרקע) — excludes bunker/רמסע function totalAmmoGuns(state) { const t = {}; (state.registeredTypes || []).forEach(r => { t[r.key] = 0; }); GUNS.forEach(g => Object.entries(state.guns[g] || {}).forEach(([k, v]) => { t[k] = (t[k] || 0) + v; })); return t; } // Bunker only (רמסע / משטחים) function totalAmmoBunker(state) { const t = {}; (state.registeredTypes || []).forEach(r => { t[r.key] = 0; }); (state.bunker || []).forEach(p => Object.entries(p.ammo || {}).forEach(([k, v]) => { t[k] = (t[k] || 0) + v; })); return t; } // Calculate totals for an ammo group: { gunTotal, bunkerTotal, total } function calcGroupTotal(group, state) { const gunT = totalAmmoGuns(state); const bunT = totalAmmoBunker(state); let gunTotal = 0, bunkerTotal = 0; (group.keys || []).forEach(k => { gunTotal += gunT[k] || 0; bunkerTotal += bunT[k] || 0; }); return { gunTotal, bunkerTotal, total: gunTotal + bunkerTotal }; } function csvExport(headers, rows, filename) { const bom = '\uFEFF'; const body = [headers, ...rows].map(r => r.map(c => `"${String(c || '').replace(/"/g, '""')}"`).join(',')).join('\n'); const blob = new Blob([bom + body], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } Object.assign(window, { GUNS, CHARGE_BEHAVIORS, CHARGE_TYPE_NAMES, makeShellKey, makeFuzeKey, makeChargeKey, parseKey, keyLabel, keyUnit, makeTypeRecord, chargeInvDeduction, LS_KEY, M85_DEFAULT, buildInit, applyMigrations, saveState, mergeRegs, reducer, genId, genMissionId, fmtDatetime, ammoQtyBadge, totalAmmo, totalAmmoGuns, totalAmmoBunker, calcGroupTotal, csvExport, });