/* ═══════════════════════════════════════════════════════════
Views — Teams (inventory matrix), Bunker, Settings (catalog)
FIXES:
- Catalog delete now works even when ammo is in use:
shows confirmation modal, removes from all guns + bunker
- Rename/remap: merge one ammo type into another (fixes
name mismatches after import)
- Reset single gun: zero out all ammo for one gun
- Delete buttons always clickable (no longer disabled when inUse)
═══════════════════════════════════════════════════════════ */
(function () {
const { useState: useStateV } = React;
const {
GUNS: GUNS_V, CHARGE_BEHAVIORS: CB_V, CHARGE_TYPE_NAMES: CTN_V,
parseKey: parseKeyV, keyLabel: keyLabelV, ammoQtyBadge: badgeV,
totalAmmo: totalAmmoV, totalAmmoGuns: totalAmmoGunsV, totalAmmoBunker: totalAmmoBunkerV,
calcGroupTotal: calcGroupTotalV,
makeShellKey: mskV, makeFuzeKey: mfkV, makeChargeKey: mckV, makeTypeRecord: mtrV,
genId: genIdV, Modal: ModalV,
} = window;
// go/GoGitMerge icon
function MergeIcon({ size = 14 }) {
return (
);
}
const CAT_META = {
shell: { label: 'פגזים', one: 'פגז', icon: '🎯', unit: 'פגזים' },
charge: { label: 'חנ"ה', one: 'חנ"ה', icon: '⚡', unit: 'יחידות' },
fuze: { label: 'מרעומים', one: 'מרעום', icon: '🟡', unit: 'מרעומים' },
};
const CAT_ORDER = ['shell', 'charge', 'fuze'];
function readinessTier(state, g) {
if (state.readiness?.red === g) return { cls: 'dot-danger', label: 'כוננות צוות אדום' };
if (state.readiness?.yellow === g) return { cls: 'dot-warn', label: 'כוננות צוות צהוב' };
return { cls: 'dot-ok', label: 'כוננות צוות ירוק' };
}
// ── Teams / inventory matrix ─────────────────────────────────
function TeamsView({ state, dispatch }) {
const regs = state.registeredTypes;
const totals = totalAmmoV(state); // all (guns + bunker)
const totalsGun = totalAmmoGunsV(state); // on-ground only
const totalsRamsav = totalAmmoBunkerV(state); // רמסע/bunker only
const setRedLine = (key, v) => dispatch({ type: 'SET_RED_LINE', key, value: Math.max(0, parseInt(v) || 0) });
const setReadiness = (level, gun) => dispatch({ type: 'SET_READINESS', level, gun });
const [filter, setFilter] = useStateV('all');
const [resetGun, setResetGun] = useStateV(null);
const doResetGun = () => {
if (!resetGun) return;
dispatch({ type: 'RESET_GUN_INVENTORY', gun: resetGun });
setResetGun(null);
};
// ── Single gun card view ──────────────────────────────────
function GunDetailView({ gun }) {
const inv = state.guns[gun] || {};
const tier = readinessTier(state, gun);
return (
צוות {gun}
{tier.label}
setResetGun(gun)}>↺ אפס מלאי
{CAT_ORDER.map(cat => {
const rows = regs.filter(r => r.category === cat);
if (!rows.length) return null;
return (
{CAT_META[cat].icon} {CAT_META[cat].label}
);
})}
);
}
// ── Full matrix: guns as rows, ammo as columns ────────────
function AllGunsMatrix() {
return (
{CAT_ORDER.map(cat => {
const cols = regs.filter(r => r.category === cat);
if (!cols.length) return null;
return (
{CAT_META[cat].icon} {CAT_META[cat].label}
{cols.length} סוגים
);
})}
);
}
return (
{/* ── Readiness + filter tabs in one bar ── */}
מטריצת מלאי צוותים
{/* Filter tabs */}
setFilter('all')}
className={'btn btn-sm ' + (filter === 'all' ? 'btn-primary' : 'btn-outline')}>
כל הצוותים
{GUNS_V.map(g => {
const t = readinessTier(state, g);
const isActive = filter === g;
return (
setFilter(g)}
className={'btn btn-sm ' + (isActive ? 'btn-amber' : 'btn-ghost')}
style={{ borderColor: isActive ? 'var(--accent)' : 'var(--border)' }}>
{g}
);
})}
{/* Readiness selectors */}
🔴 כוננות אדומה
setReadiness('red', e.target.value)}>
— ללא — {GUNS_V.map(g => צוות {g} )}
🟡 כוננות צהובה
setReadiness('yellow', e.target.value)}>
— ללא — {GUNS_V.map(g => צוות {g} )}
{/* ── Content: single gun or full matrix ── */}
{filter === 'all' ?
:
}
{/* Reset gun confirmation */}
{resetGun && (
setResetGun(null)}>
⚠ כל התחמושת של צוות {resetGun} תאופס לאפס. פינת פסולים לא תושפע. פעולה זו אינה הפיכה.
setResetGun(null)}>ביטול
↺ אפס מלאי צוות {resetGun}
)}
);
}
// ── Bunker / pallets ─────────────────────────────────────────
function EditPalletModal({ pallet, regs, dispatch, onClose }) {
const [qtys, setQtys] = useStateV(() => { const o = {}; regs.forEach(r => { o[r.key] = String(pallet.ammo[r.key] || 0); }); return o; });
const submit = () => {
const ammo = {};
regs.forEach(r => { const q = parseInt(qtys[r.key]) || 0; if (q > 0) ammo[r.key] = q; });
dispatch({ type: 'EDIT_PALLET_STOCK', palletId: pallet.id, ammo });
onClose();
};
return (
{regs.map(r => (
{r.label}
{r.unit}
setQtys(p => ({ ...p, [r.key]: e.target.value }))} style={{ width: '78px', textAlign: 'center' }} />
))}
ביטול
💾 שמור
);
}
function BunkerView({ state, dispatch, onAddPallet }) {
const regs = state.registeredTypes;
const [editing, setEditing] = useStateV(null);
const [confirmClear, setConfirmClear] = useStateV(null);
return (
מחסן ומשטחים
+ קליטת משטח
{state.bunker.length === 0 ? (
📦
אין משטחים במחסן — קלוט משטח חדש
) : (
{state.bunker.map(p => {
const items = regs.filter(r => (p.ammo[r.key] || 0) > 0);
const sum = items.reduce((s, r) => s + (p.ammo[r.key] || 0), 0);
return (
משטח #{p.id}
{sum} פריטים
{items.length === 0 ?
ריק : items.map(r => (
{r.label} {badgeV(p.ammo[r.key])}
))}
setEditing(p)}>✎ ערוך
setConfirmClear(p)}>🗑 רוקן
);
})}
)}
{editing &&
setEditing(null)} />}
{confirmClear && (
setConfirmClear(null)}>
המשטח יוסר לצמיתות מהמחסן. פעולה זו אינה הפיכה.
setConfirmClear(null)}>ביטול
{ dispatch({ type: 'CLEAR_PALLET', palletId: confirmClear.id }); setConfirmClear(null); }}>🗑 אשר רוקון
)}
);
}
// ── Settings / central catalog ───────────────────────────────
function AddTypeRow({ cat, regs, dispatch }) {
const [shellName, setShellName] = useStateV('');
const [fuzeName, setFuzeName] = useStateV('');
const [chargeType, setChargeType] = useStateV(CTN_V[0]);
const [series, setSeries] = useStateV('');
const buildRecord = () => {
if (cat === 'shell') { const n = shellName.trim(); if (!n) return null; return mtrV(mskV(n)); }
if (cat === 'fuze') { const n = fuzeName.trim(); if (!n) return null; return mtrV(mfkV(n)); }
const s = series.trim(); if (!s) return null; return mtrV(mckV(chargeType, s));
};
const rec = buildRecord();
const exists = rec && regs.find(r => r.key === rec.key);
const add = () => { if (!rec || exists) return; dispatch({ type: 'ADD_REGISTERED_TYPE', record: rec }); setShellName(''); setFuzeName(''); setSeries(''); };
return (
{exists &&
⚠ סוג זה כבר קיים בקטלוג
}
);
}
// ── Remap modal: merge srcKey inventory into dstKey ──────────
// Used to fix name mismatches after import. Moves all qty from
// src to dst across all guns + bunker, then removes src from catalog.
function RemapModal({ srcRec, regs, dispatch, onClose }) {
const [dstKey, setDstKey] = useStateV('');
const samecat = regs.filter(r => r.key !== srcRec.key && r.category === srcRec.category);
const canSubmit = !!dstKey;
const doRemap = () => {
if (!dstKey) return;
dispatch({ type: 'REMAP_AMMO_TYPE', srcKey: srcRec.key, dstKey });
onClose();
};
return (
כל הכמויות של {srcRec.label} (בצוותים + מחסן) יועברו לסוג היעד שתבחר, ו-{srcRec.label} יוסר מהקטלוג. שימושי לתיקון שמות לאחר ייבוא.
העבר אל (סוג יעד — אותה קטגוריה)
setDstKey(e.target.value)}>
— בחר סוג יעד —
{samecat.map(r => {r.label} )}
{!samecat.length && (
אין סוגים אחרים באותה קטגוריה. הוסף קודם את סוג היעד לקטלוג.
)}
ביטול
🔀 בצע מיזוג
);
}
// ── Delete confirmation modal (force-delete even if in use) ──
function DeleteConfirmModal({ rec, totals, dispatch, onClose }) {
const qty = totals[rec.key] || 0;
const doDelete = () => {
dispatch({ type: 'FORCE_DELETE_TYPE', key: rec.key });
onClose();
};
return (
{qty > 0 ? (
⚠ קיים מלאי פעיל: {qty} יח' של {rec.label} בצוותים ובמחסן.
המחיקה תסיר את הסוג מהקטלוג ותמחק את כל הכמויות מכל הצוותים והמשטחים.
) : (
מחיקת {rec.label} מהקטלוג. אין מלאי פעיל.
)}
ביטול
🗑 אשר מחיקה
);
}
function ConfigView({ state, dispatch }) {
const regs = state.registeredTypes;
const totals = totalAmmoV(state);
const [editKey, setEditKey] = useStateV(null);
const [draft, setDraft] = useStateV('');
const [deleteRec, setDeleteRec] = useStateV(null);
const [remapRec, setRemapRec] = useStateV(null);
const M85 = window.M85_DEFAULT.key;
const startEdit = r => { setEditKey(r.key); setDraft(r.label); };
const saveEdit = () => { if (draft.trim()) dispatch({ type: 'UPDATE_REGISTERED_TYPE', key: editKey, label: draft.trim() }); setEditKey(null); };
return (
קטלוג תחמושת מרכזי
זהו המקור היחיד לשמות הפגזים, החנ"ות והמרעומים. כל הטפסים, החישובים והדוחות במערכת יורשים את הקטלוג הזה.
{CAT_ORDER.map(cat => {
const rows = regs.filter(r => r.category === cat);
return (
{CAT_META[cat].icon} {CAT_META[cat].label}
{rows.length} סוגים
{rows.length === 0 ?
אין סוגים — הוסף למטה
: (
)}
);
})}
{deleteRec && (
setDeleteRec(null)} />
)}
{remapRec && (
setRemapRec(null)} />
)}
{/* ── Ammo groups panel ── */}
);
}
// ── Ammo groups management ───────────────────────────────────
// Groups let you define a named collection of ammo types
// (e.g. "פגזי שדה" = OF-540 + M107) and set a red line on
// the COMBINED total (on-ground + רמסע).
function AmmoGroupsPanel({ state, dispatch }) {
const regs = state.registeredTypes;
const groups = state.ammoGroups || [];
const [editing, setEditing] = useStateV(null); // group being edited
const [adding, setAdding] = useStateV(false); // show add form
return (
📦 קבוצות תחמושת
setAdding(true)}>+ קבוצה חדשה
הגדר קבוצות של סוגי תחמושת לניטור מצטבר עם קו אדום על הסה"כ (קרקע + רמסע).
{groups.length === 0 && !adding && (
אין קבוצות — הוסף קבוצה חדשה
)}
{groups.map(g => {
const { gunTotal, bunkerTotal, total } = calcGroupTotalV(g, state);
const crit = g.redLine > 0 && total < g.redLine;
return editing?.id === g.id
?
setEditing(null)} />
: (
{g.label} {crit && ⚠ מתחת לקו }
{(g.keys || []).map(k => keyLabelV(k, regs)).join(' · ')}
{gunTotal}
קרקע
{bunkerTotal}
רמסע
{total}
סה"כ{g.redLine > 0 ? ` / קו ${g.redLine}` : ''}
setEditing(g)}>✎ ערוך
dispatch({ type: 'DELETE_AMMO_GROUP', id: g.id })}>🗑
);
})}
{adding && (
setAdding(false)} isNew />
)}
);
}
function GroupEditForm({ group, regs, dispatch, onClose, isNew }) {
const [label, setLabel] = useStateV(group?.label || '');
const [keys, setKeys] = useStateV(group?.keys || []);
const [redLine, setRedLine] = useStateV(String(group?.redLine || 0));
const toggleKey = k => setKeys(prev => prev.includes(k) ? prev.filter(x => x !== k) : [...prev, k]);
const save = () => {
if (!label.trim() || keys.length === 0) return;
const rl = parseInt(redLine) || 0;
if (isNew) {
dispatch({ type: 'ADD_AMMO_GROUP', label: label.trim(), keys, redLine: rl });
} else {
dispatch({ type: 'EDIT_AMMO_GROUP', id: group.id, label: label.trim(), keys, redLine: rl });
}
onClose();
};
return (
סוגי תחמושת בקבוצה
{regs.map(r => {
const selected = keys.includes(r.key);
return (
toggleKey(r.key)}
className={'btn btn-sm ' + (selected ? 'btn-primary' : 'btn-outline')}
style={{ fontSize: '.75rem' }}>
{selected ? '✓ ' : ''}{r.label}
);
})}
{keys.length === 0 &&
בחר לפחות סוג אחד }
ביטול
{isNew ? '+ צור קבוצה' : '💾 שמור'}
);
}
window.TeamsView = TeamsView;
window.BunkerView = BunkerView;
window.ConfigView = ConfigView;
window.AmmoGroupsPanel = AmmoGroupsPanel;
})();