PageSourceSearch

https://neeharaa.com/tools/consumption-costing.html

html neeharaa.com collected 2026-09-30 04:24:47 UTC 97,408 bytes, 1,492 lines download raw bytes

1<!DOCTYPE html>
2<!-- Proprietary tool. (c) Maneesh. Not for reuse or redistribution. -->
3<html lang="en">
4<head>
5<meta charset="utf-8">
6<meta name="viewport" content="width=device-width, initial-scale=1">
7<title>Yield &amp; Cost Control — Recipe Consumption and Product Costing</title>
8<link rel="preconnect" href="https://fonts.googleapis.com">
9<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10<link href="https://fonts.googleapis.com/css2?family=Archivo:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet">
11<style>
12:root{
13  --ink:#101619;
14  --ink2:#4a585f;
15  --ink3:#7d8a90;
16  --line:#d3dade;
17  --line2:#e6eaec;
18  --paper:#e9edee;
19  --surface:#ffffff;
20  --accent:#0d5b4e;
21  --accent2:#118a75;
22  --accent-soft:#e2efec;
23  --amber:#8f5a0c;
24  --amber-soft:#f6ecd9;
25  --red:#9d2c2c;
26  --red-soft:#f7e5e5;
27  --radius:4px;
28  --shadow:0 1px 2px rgba(16,22,25,.06),0 8px 24px -12px rgba(16,22,25,.18);
29}
30*{box-sizing:border-box}
31html,body{margin:0;padding:0}
32body{
33  background:var(--paper);
34  color:var(--ink);
35  font-family:"IBM Plex Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
36  font-size:15px;line-height:1.5;
37  -webkit-font-smoothing:antialiased;
38}
39h1,h2,h3,h4{font-family:Archivo,"IBM Plex Sans",sans-serif;margin:0;letter-spacing:-.02em}
40.num,input[type=number],.mono{font-family:"IBM Plex Mono",ui-monospace,Menlo,monospace;font-variant-numeric:tabular-nums}
41
42/* ---------- masthead ---------- */
43.mast{background:var(--ink);color:#fff;padding:18px 20px 0}
44.mast-in{max-width:1180px;margin:0 auto}
45.eyebrow{font-family:"IBM Plex Mono",monospace;font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:#8fb3ac}
46.mast h1{font-size:26px;font-weight:700;margin:2px 0 2px;color:#fff}
47.mast p{margin:0 0 14px;color:#9fb0b6;font-size:13.5px;max-width:62ch}
48.tabs{display:flex;gap:2px;overflow-x:auto;scrollbar-width:none}
49.tabs::-webkit-scrollbar{display:none}
50.tab{
51  appearance:none;border:0;background:#1c262b;color:#a9bcc2;
52  font:600 13px/1 "IBM Plex Sans",sans-serif;letter-spacing:.01em;
53  padding:12px 15px;border-radius:var(--radius) var(--radius) 0 0;cursor:pointer;white-space:nowrap;
54}
55.tab:hover{background:#253238;color:#fff}
56.tab[aria-selected=true]{background:var(--paper);color:var(--ink)}
57.tab .dot{display:inline-block;min-width:17px;height:17px;line-height:17px;border-radius:9px;background:#33444b;color:#cfe0e4;font-family:"IBM Plex Mono",monospace;font-size:10.5px;text-align:center;margin-left:7px;padding:0 4px}
58.tab[aria-selected=true] .dot{background:var(--accent);color:#fff}
59
60/* ---------- layout ---------- */
61.wrap{max-width:1180px;margin:0 auto;padding:22px 20px 90px}
62.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);margin-bottom:18px}
63.panel-hd{display:flex;align-items:flex-start;justify-content:space-between;gap:14px;padding:15px 17px;border-bottom:1px solid var(--line2);flex-wrap:wrap}
64.panel-hd h2{font-size:16.5px;font-weight:600}
65.panel-hd .sub{font-size:12.5px;color:var(--ink3);margin-top:3px;max-width:66ch}
66.panel-bd{padding:17px}
67.panel-bd.tight{padding:0}
68.hint{font-size:12.5px;color:var(--ink3)}
69
70/* ---------- forms ---------- */
71.grid{display:grid;gap:12px}
72.g2{grid-template-columns:repeat(2,1fr)}
73.g3{grid-template-columns:repeat(3,1fr)}
74.g4{grid-template-columns:repeat(4,1fr)}
75label{display:block;font-size:11.5px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--ink2);margin-bottom:5px}
76input,select,textarea{
77  width:100%;padding:9px 10px;border:1px solid var(--line);border-radius:var(--radius);
78  background:#fff;color:var(--ink);font-size:14.5px;font-family:inherit;
79}
80input:focus,select:focus,textarea:focus{outline:2px solid var(--accent2);outline-offset:-1px;border-color:var(--accent2)}
81input[type=number]{text-align:right}
82.btn{
83  appearance:none;border:1px solid var(--ink);background:var(--ink);color:#fff;
84  font:600 13.5px/1 "IBM Plex Sans",sans-serif;padding:10px 15px;border-radius:var(--radius);cursor:pointer;
85}
86.btn:hover{background:#000}
87.btn.primary{background:var(--accent);border-color:var(--accent)}
88.btn.primary:hover{background:var(--accent2);border-color:var(--accent2)}
89.btn.ghost{background:transparent;color:var(--ink);border-color:var(--line)}
90.btn.ghost:hover{background:var(--paper)}
91.btn.sm{padding:7px 11px;font-size:12.5px}
92.btn.danger{background:transparent;color:var(--red);border-color:#e3c9c9}
93.btn.danger:hover{background:var(--red-soft)}
94.btn:focus-visible,.tab:focus-visible,.linkbtn:focus-visible{outline:2px solid var(--accent2);outline-offset:2px}
95.row-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
96.linkbtn{background:none;border:0;color:var(--accent);font:600 13px/1 inherit;cursor:pointer;padding:4px 2px;text-decoration:underline;text-underline-offset:3px}
97
98/* ---------- tables ---------- */
99.tscroll{overflow-x:auto}
100table{width:100%;border-collapse:collapse;font-size:14px}
101th{
102  text-align:left;font-size:11px;letter-spacing:.07em;text-transform:uppercase;color:var(--ink2);
103  font-weight:600;padding:10px 12px;border-bottom:1px solid var(--line);background:#f6f8f8;white-space:nowrap;
104}
105td{padding:10px 12px;border-bottom:1px solid var(--line2);vertical-align:middle}
106tbody tr:last-child td{border-bottom:0}
107tbody tr:hover{background:#fafcfc}
108td.n,th.n{text-align:right;font-family:"IBM Plex Mono",monospace;font-variant-numeric:tabular-nums;white-space:nowrap}
109tfoot td{border-top:2px solid var(--ink);border-bottom:0;font-weight:600;background:#f6f8f8}
110.tname{font-weight:600}
111.tsub{font-size:12px;color:var(--ink3)}
112td input{padding:6px 8px;font-size:13.5px}
113
114/* ---------- empty state ---------- */
115.empty{padding:34px 20px;text-align:center;color:var(--ink3)}
116.empty strong{display:block;color:var(--ink);font-family:Archivo,sans-serif;font-size:15.5px;margin-bottom:5px}
117
118/* ---------- stat strip ---------- */
119.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:1px;background:var(--line2)}
120.stat{background:#fff;padding:15px 16px}
121.stat .k{font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--ink3);font-weight:600}
122.stat .v{font-family:"IBM Plex Mono",monospace;font-size:22px;font-weight:600;margin-top:5px;letter-spacing:-.02em}
123.stat .m{font-size:12px;color:var(--ink3);margin-top:2px}
124.v.good{color:var(--accent)}.v.warn{color:var(--amber)}.v.bad{color:var(--red)}
125
126/* ---------- signature: variance meter ---------- */
127.meter{position:relative;height:22px;background:#f2f5f5;border:1px solid var(--line2);border-radius:2px;min-width:150px}
128.meter .theo{position:absolute;top:0;bottom:0;left:0;background:var(--accent-soft);border-right:2px solid var(--accent)}
129.meter .act{position:absolute;top:5px;height:12px;left:0;background:var(--ink);opacity:.72;border-radius:1px}
130.meter .over{position:absolute;top:0;bottom:0;background:repeating-linear-gradient(45deg,var(--red-soft),var(--red-soft) 4px,#fff 4px,#fff 8px);border-left:1px solid var(--red)}
131.meter-lgd{display:flex;gap:16px;flex-wrap:wrap;font-size:11.5px;color:var(--ink2);padding:11px 12px 0}
132.swatch{display:inline-block;width:11px;height:11px;border-radius:2px;margin-right:5px;vertical-align:-1px}
133.tag{display:inline-block;font-family:"IBM Plex Mono",monospace;font-size:11px;padding:2px 6px;border-radius:2px;font-weight:500}
134.tag.ok{background:var(--accent-soft);color:var(--accent)}
135.tag.warn{background:var(--amber-soft);color:var(--amber)}
136.tag.bad{background:var(--red-soft);color:var(--red)}
137
138/* ---------- cost ladder ---------- */
139.ladder{display:flex;height:26px;border-radius:2px;overflow:hidden;border:1px solid var(--line)}
140.ladder span{display:block;height:100%}
141.ladder-key{display:flex;flex-wrap:wrap;gap:12px;margin-top:9px;font-size:12px;color:var(--ink2)}
142
143/* ---------- modal ---------- */
144.ovl{position:fixed;inset:0;background:rgba(16,22,25,.55);display:flex;align-items:flex-start;justify-content:center;padding:24px 14px;overflow-y:auto;z-index:50}
145.ovl[hidden]{display:none}
146.modal{background:#fff;border-radius:var(--radius);width:100%;max-width:840px;box-shadow:var(--shadow)}
147.modal-hd{display:flex;justify-content:space-between;align-items:center;padding:15px 18px;border-bottom:1px solid var(--line2)}
148.modal-hd h3{font-size:17px}
149.modal-bd{padding:18px}
150.modal-ft{padding:14px 18px;border-top:1px solid var(--line2);display:flex;justify-content:flex-end;gap:9px;flex-wrap:wrap}
151.xbtn{border:0;background:none;font-size:22px;line-height:1;cursor:pointer;color:var(--ink3);padding:4px 8px}
152
153/* ---------- misc ---------- */
154.toast{position:fixed;left:50%;transform:translateX(-50%);bottom:26px;background:var(--ink);color:#fff;padding:11px 17px;border-radius:var(--radius);font-size:13.5px;box-shadow:var(--shadow);z-index:80;max-width:90vw}
155.toast[hidden]{display:none}
156.toast.err{background:var(--red)}
157.filebox{border:1.5px dashed var(--line);border-radius:var(--radius);padding:20px;text-align:center;background:#fbfcfc}
158.filebox.drag{border-color:var(--accent2);background:var(--accent-soft)}
159.note{background:#f6f8f8;border-left:3px solid var(--accent);padding:11px 13px;font-size:13px;color:var(--ink2);border-radius:0 var(--radius) var(--radius) 0}
160.note code{background:#e7ecec;padding:1px 4px;border-radius:2px;font-size:12.5px}
161.log{max-height:210px;overflow:auto;font-family:"IBM Plex Mono",monospace;font-size:12px;background:#fbfcfc;border:1px solid var(--line2);border-radius:var(--radius);padding:10px}
162.log div{padding:2px 0;border-bottom:1px solid var(--line2)}
163.log div:last-child{border-bottom:0}
164.log .bad{color:var(--red)}
165.log .ok{color:var(--accent)}
166ol.steps{margin:0;padding-left:19px}
167ol.steps li{margin-bottom:7px}
168.pill{display:inline-block;background:var(--paper);border:1px solid var(--line);border-radius:999px;padding:3px 10px;font-size:12px;margin:0 5px 5px 0}
169@media (max-width:820px){
170  .g3,.g4{grid-template-columns:repeat(2,1fr)}
171  .stats{grid-template-columns:repeat(2,1fr)}
172  .mast h1{font-size:22px}
173}
174@media (max-width:520px){
175  .g2,.g3,.g4{grid-template-columns:1fr}
176  .wrap{padding:16px 13px 80px}
177}
178@media (prefers-reduced-motion:no-preference){
179  .panel{animation:rise .22s ease both}
180  @keyframes rise{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
181}
182@media print{
183  .mast,.tabs,.btn,.row-actions{display:none!important}
184  body{background:#fff}
185  .panel{border-color:#bbb;break-inside:avoid}
186}
187</style>
188</head>
189<body>
190
191<header class="mast">
192  <div class="mast-in">
193    <div class="eyebrow">Yield &amp; cost control</div>
194    <h1>Consumption &amp; Product Costing</h1>
195    <p>Work out what your recipes should have consumed, compare it with what actually left the store, and cost every finished good down to the packet.</p>
196    <nav class="tabs" role="tablist" id="tabs">
197      <button class="tab" role="tab" data-tab="materials" aria-selected="true">Raw materials <span class="dot" id="c-mat">0</span></button>
198      <button class="tab" role="tab" data-tab="recipes" aria-selected="false">Recipes <span class="dot" id="c-rec">0</span></button>
199      <button class="tab" role="tab" data-tab="sales" aria-selected="false">Sales <span class="dot" id="c-sal">0</span></button>
200      <button class="tab" role="tab" data-tab="production" aria-selected="false">Production <span class="dot" id="c-bat">0</span></button>
201      <button class="tab" role="tab" data-tab="reports" aria-selected="false">Reports</button>
202      <button class="tab" role="tab" data-tab="data" aria-selected="false">Excel &amp; backup</button>
203    </nav>
204  </div>
205</header>
206
207<main class="wrap">
208
209<!-- ============ MATERIALS ============ -->
210<section id="p-materials" class="page">
211  <div class="panel">
212    <div class="panel-hd">
213      <div>
214        <h2>Add a raw material</h2>
215        <div class="sub">Everything else is priced from this list. Enter the rate in the unit you buy in — kilos, litres or pieces. Recipes can still be written in grams or millilitres.</div>
216      </div>
217    </div>
218    <div class="panel-bd">
219      <div class="grid g4">
220        <div><label for="m-code">Code</label><input id="m-code" placeholder="RM001"></div>
221        <div><label for="m-name">Material name</label><input id="m-name" placeholder="Chicken, boneless"></div>
222        <div><label for="m-cat">Category</label><input id="m-cat" placeholder="Meat" list="catlist"><datalist id="catlist"></datalist></div>
223        <div><label for="m-unit">Purchase unit</label><select id="m-unit">
224          <option value="kg">kg — kilogram</option><option value="g">g — gram</option>
225          <option value="l">L — litre</option><option value="ml">ml — millilitre</option>
226          <option value="nos">nos — pieces</option></select></div>
227        <div><label for="m-rate">Rate per unit</label>
227<input id="m-rate" type="number" step="0.01" min="0" placeholder="0.00"></div>
228        <div style="display:flex;align-items:flex-end"><button class="btn primary" id="m-add">Add material</button></div>
229      </div>
230    </div>
231  </div>
232
233  <div class="panel">
234    <div class="panel-hd">
235      <div><h2>Raw material list</h2><div class="sub" id="m-count"></div></div>
236      <div class="row-actions">
237        <input id="m-search" placeholder="Search name or code" style="width:210px">
238        <button class="btn ghost sm" id="m-csv">Download PDF</button>
239      </div>
240    </div>
241    <div class="panel-bd tight"><div class="tscroll"><table id="m-table">
242      <thead><tr><th>Code</th><th>Material</th><th>Category</th><th>Unit</th><th class="n">Rate</th><th class="n">Used in</th><th></th></tr></thead>
243      <tbody></tbody></table></div></div>
244  </div>
245</section>
246
247<!-- ============ RECIPES ============ -->
248<section id="p-recipes" class="page" hidden>
249  <div class="panel">
250    <div class="panel-hd">
251      <div>
252        <h2>Menu item recipes</h2>
253        <div class="sub">A recipe is the standard quantity of each raw material that one portion of a menu item should use. This is what turns your sales figures into consumption.</div>
254      </div>
255      <button class="btn primary" id="r-new">New recipe</button>
256    </div>
257    <div class="panel-bd tight"><div class="tscroll"><table id="r-table">
258      <thead><tr><th>Code</th><th>Menu item</th><th class="n">Ingredients</th><th class="n">Cost / portion</th><th class="n">Selling price</th><th class="n">Food cost %</th><th></th></tr></thead>
259      <tbody></tbody></table></div></div>
260  </div>
261</section>
262
263<!-- ============ SALES ============ -->
264<section id="p-sales" class="page" hidden>
265  <div class="panel">
266    <div class="panel-hd">
267      <div><h2>Record item sales</h2><div class="sub">Enter the quantity sold of each menu item. You can also bring a whole month in from Excel on the last tab.</div></div>
268    </div>
269    <div class="panel-bd">
270      <div class="grid g4">
271        <div><label for="s-date">Date</label><input id="s-date" type="date"></div>
272        <div style="grid-column:span 2"><label for="s-item">Menu item</label><select id="s-item"></select></div>
273        <div><label for="s-qty">Quantity sold</label><input id="s-qty" type="number" step="0.01" min="0" placeholder="0"></div>
274      </div>
275      <div style="margin-top:12px"><button class="btn primary" id="s-add">Add sale</button></div>
276    </div>
277  </div>
278  <div class="panel">
279    <div class="panel-hd">
280      <div><h2>Sales entries</h2><div class="sub" id="s-count"></div></div>
281      <div class="row-actions">
282        <button class="btn ghost sm" id="s-csv">Download PDF</button>
283        <button class="btn danger sm" id="s-clear">Clear all sales</button>
284      </div>
285    </div>
286    <div class="panel-bd tight"><div class="tscroll"><table id="s-table">
287      <thead><tr><th>Date</th><th>Menu item</th><th class="n">Qty sold</th><th class="n">Revenue</th><th class="n">Recipe cost</th><th></th></tr></thead>
288      <tbody></tbody><tfoot></tfoot></table></div></div>
289  </div>
290</section>
291
292<!-- ============ PRODUCTION ============ -->
293<section id="p-production" class="page" hidden>
294  <div class="panel">
295    <div class="panel-hd">
296      <div>
297        <h2>Production batches</h2>
298        <div class="sub">For packed food. Enter the raw materials that went into one production run and how much finished good came out of it — the tool works back to the cost of a single pack.</div>
299      </div>
300      <button class="btn primary" id="b-new">New batch</button>
301    </div>
302    <div class="panel-bd tight"><div class="tscroll"><table id="b-table">
303      <thead><tr><th>Batch</th><th>Date</th><th>Finished good</th><th class="n">Output</th><th class="n">Material cost</th><th class="n">Other cost</th><th class="n">Total cost</th><th class="n">Cost / unit</th><th></th></tr></thead>
304      <tbody></tbody></table></div></div>
305  </div>
306  <div id="b-detail"></div>
307</section>
308
309<!-- ============ REPORTS ============ -->
310<section id="p-reports" class="page" hidden>
311  <div class="panel">
312    <div class="panel-hd">
313      <div><h2>Reporting period</h2><div class="sub">Leave the dates blank to include every sales entry.</div></div>
314      <div class="row-actions">
315        <input id="f-from" type="date" style="width:auto"><span class="hint">to</span><input id="f-to" type="date" style="width:auto">
316        <button class="btn ghost sm" id="f-clear">Reset</button>
317        <button class="btn ghost sm" id="r-print">Print / save PDF</button>
318      </div>
319    </div>
320    <div class="panel-bd tight"><div class="stats" id="r-stats"></div></div>
321  </div>
322
323  <div class="panel">
324    <div class="panel-hd">
325      <div><h2>Menu item profitability</h2><div class="sub">What each dish sold, what it cost you in raw materials, and what it left behind.</div></div>
326      <button class="btn ghost sm" id="rp-csv">Download PDF</button>
327    </div>
328    <div class="panel-bd tight"><div class="tscroll"><table id="rp-table">
329      <thead><tr><th>
329Menu item</th><th class="n">Sold</th><th class="n">Cost / portion</th><th class="n">Price</th><th class="n">Total cost</th><th class="n">Revenue</th><th class="n">Margin</th><th class="n">Food cost %</th></tr></thead>
330      <tbody></tbody><tfoot></tfoot></table></div></div>
331  </div>
332
333  <div class="panel">
334    <div class="panel-hd">
335      <div>
336        <h2>Raw material consumption &amp; variance</h2>
337        <div class="sub">The theoretical column is what your recipes say should have been used. Type in what the store actually issued and the difference shows up as pilferage, over-portioning or wastage.</div>
338      </div>
339      <button class="btn ghost sm" id="rc-csv">Download PDF</button>
340    </div>
341    <div class="meter-lgd">
342      <span><span class="swatch" style="background:var(--accent-soft);border:1px solid var(--accent)"></span>Theoretical</span>
343      <span><span class="swatch" style="background:var(--ink);opacity:.72"></span>Actual issued</span>
344      <span><span class="swatch" style="background:var(--red-soft);border:1px solid var(--red)"></span>Excess over theoretical</span>
345    </div>
346    <div class="panel-bd tight"><div class="tscroll"><table id="rc-table">
347      <thead><tr><th>Material</th><th>Unit</th><th class="n">Theoretical qty</th><th class="n">Theoretical value</th><th class="n">Actual qty</th><th class="n">Variance qty</th><th class="n">Variance value</th><th style="min-width:170px">Comparison</th></tr></thead>
348      <tbody></tbody><tfoot></tfoot></table></div></div>
349  </div>
350
351  <div class="panel">
352    <div class="panel-hd">
353      <div><h2>Finished goods costing</h2><div class="sub">Every production run, and what one unit of output cost to make.</div></div>
354      <button class="btn ghost sm" id="rb-csv">Download PDF</button>
355    </div>
356    <div class="panel-bd tight"><div class="tscroll"><table id="rb-table">
357      <thead><tr><th>Finished good</th><th class="n">Batches</th><th class="n">Total output</th><th class="n">Material cost</th><th class="n">Other cost</th><th class="n">Total cost</th><th class="n">Avg cost / unit</th></tr></thead>
358      <tbody></tbody></table></div></div>
359  </div>
360</section>
361
362<!-- ============ DATA ============ -->
363<section id="p-data" class="page" hidden>
364  <div class="panel">
365    <div class="panel-hd"><div><h2>Bring entries in from Excel</h2><div class="sub">Download the template, fill the sheets you need, then upload the same file back. Sheets you leave empty are ignored.</div></div></div>
366    <div class="panel-bd">
367      <div class="grid g2">
368        <div>
369          <ol class="steps">
370            <li><button class="linkbtn" id="x-tpl">Download the Excel template</button> — it has four sheets with the column headings already in place and a filled example row.</li>
371            <li>Delete the example rows and enter your own data. Keep the headings exactly as they are.</li>
372            <li>Upload the file below. Existing entries are kept; new ones are added on top.</li>
373          </ol>
374          <div class="note" style="margin-top:14px">Material codes tie everything together. A recipe or batch row must use a code that already exists on the <code>Materials</code> sheet, or that you are importing in the same file.</div>
375        </div>
376        <div>
377          <div class="filebox" id="drop">
378            <input type="file" id="x-file" accept=".xlsx,.xls,.csv" hidden>
379            <div style="font-weight:600;margin-bottom:6px">Drop your Excel file here</div>
380            <div class="hint" style="margin-bottom:12px">.xlsx, .xls or .csv</div>
381            <button class="btn" id="x-pick">Choose file</button>
382          </div>
383          <div id="x-log" class="log" style="margin-top:12px" hidden></div>
384        </div>
385      </div>
386    </div>
387  </div>
388
389  <div class="panel">
390    <div class="panel-hd"><div><h2>Backup and settings</h2><div class="sub">Your data is stored in this browser on this device. Take a backup file regularly, and to move to another computer just restore it there.</div></div></div>
391    <div class="panel-bd">
392      <div class="grid g3">
393        <div><label for="cfg-cur">Currency symbol</label><input id="cfg-cur" maxlength="4" placeholder="₹"></div>
394        <div style="grid-column:span 2"><label for="cfg-biz">Business name (shown on printouts)</label><input id="cfg-biz" placeholder="Anand Bhavan Restaurant"></div>
395      </div>
396      <div class="row-actions" style="margin-top:16px">
397        <button class="btn" id="d-export">Download backup</button>
398        <button class="btn ghost" id="d-import">Restore from backup</button>
399        <input type="file" id="d-file" accept=".json" hidden>
400        <button class="btn ghost" id="d-demo">Load sample data</button>
401        <button class="btn danger" id="d-wipe">Erase everything</button>
402      </div>
403      <div class="hint" id="d-status" style="margin-top:12px"></div>
404    </div>
405  </div>
406</section>
407
408</main>
409
410<!-- ============ RECIPE MODAL ============ -->
411<div class="ovl" id="ovl-r" hidden>
412  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="rm-title">
413    <div class="modal-hd"><h3 id="rm-title">Recipe</h3><button class="xbtn" data-close="ovl-r" aria-label="Close">&times;</button></div>
414    <div class="modal-bd">
415      <div class="grid g4">
416        <div><label for="rm-code">Item code</label><input id="rm-code" placeholder="M001"></div>
417        <div style="grid-column:span 2"><label for="rm-name">Menu item</label><input id="rm-name" placeholder="Chicken Biryani"></div>
418        <div><label for="rm-price">Selling price</label><input id="rm-price" type="number" step="0.01" min="0"></div>
419      </div>
420      <div style="margin-top:18px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
421        <label style="margin:0">Ingredients for one portion</label>
422        <button class="btn ghost sm" id="rm-addline">Add ingredient</button>
423      </div>
424      <div class="tscroll" style="margin-top:8px"><table id="rm-lines">
425        <thead><tr><th style="min-width:190px">Raw material</th><th class="n" style="width:110px">Quantity</th><th style="width:90px">Unit</th><th class="n">Cost</th><th style="width:40px"></th></tr></thead>
426        <tbody></tbody><tfoot></tfoot></table></div>
427    </div>
428    <div class="modal-ft">
429      <button class="btn ghost" data-close="ovl-r">Cancel</button>
430      <button class="btn primary" id="rm-save">Save recipe</button>
431    </div>
432  </div>
433</div>
434
435<!-- ============ BATCH MODAL ============ -->
436<div class="ovl" id="ovl-b" hidden>
437  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="bm-title">
438    <div class="modal-hd"><h3 id="bm-title">Production batch</h3><button class="xbtn" data-close="ovl-b" aria-label="Close">&times;</button></div>
439    <div class="modal-bd">
440      <div class="grid g4">
441        <div><label for="bm-no">Batch number</label><input id="bm-no" placeholder="B-2026-014"></div>
442        <div><label for="bm-date">Production date</label><input id="bm-date" type="date"></div>
443        <div style="grid-column:span 2"><label for="bm-prod">Finished good</label><input id="bm-prod" placeholder="Banana chips 200g pack"></div>
444        <div><label for="bm-qty">Output quantity</label><input id="bm-qty" type="number" step="0.001" min="0" placeholder="0"></div>
445        <div><label for="bm-unit">Output unit</label><input id="bm-unit" placeholder="packs" list="unitlist"><datalist id="unitlist"><option value="packs"><option value="kg"><option value="litres"><option value="boxes"><option value="nos"></datalist></div>
446        <div style="grid-column:span 2" class="hint" id="bm-note"></div>
447      </div>
448
449      <div style="margin-top:18px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px">
450        <label style="margin:0">
450Raw materials consumed in this batch</label>
451        <button class="btn ghost sm" id="bm-addline">Add material</button>
452      </div>
453      <div class="tscroll" style="margin-top:8px"><table id="bm-lines">
454        <thead><tr><th style="min-width:190px">Raw material</th><th class="n" style="width:110px">Quantity</th><th style="width:90px">Unit</th><th class="n">Cost</th><th style="width:40px"></th></tr></thead>
455        <tbody></tbody><tfoot></tfoot></table></div>
456
457      <div style="margin-top:18px"><label>Other costs for this batch</label>
458        <div class="grid g4">
459          <div><label for="bm-lab" style="font-weight:400;text-transform:none;letter-spacing:0;font-size:12px">Labour</label><input id="bm-lab" type="number" step="0.01" min="0" placeholder="0"></div>
460          <div><label for="bm-pak" style="font-weight:400;text-transform:none;letter-spacing:0;font-size:12px">Packaging</label><input id="bm-pak" type="number" step="0.01" min="0" placeholder="0"></div>
461          <div><label for="bm-utl" style="font-weight:400;text-transform:none;letter-spacing:0;font-size:12px">Power &amp; fuel</label><input id="bm-utl" type="number" step="0.01" min="0" placeholder="0"></div>
462          <div><label for="bm-oth" style="font-weight:400;text-transform:none;letter-spacing:0;font-size:12px">Other overheads</label><input id="bm-oth" type="number" step="0.01" min="0" placeholder="0"></div>
463        </div>
464      </div>
465    </div>
466    <div class="modal-ft">
467      <button class="btn ghost" data-close="ovl-b">Cancel</button>
468      <button class="btn primary" id="bm-save">Save batch</button>
469    </div>
470  </div>
471</div>
472
473<div class="toast" id="toast" hidden></div>
474<script>
475/*! Consumption & Product Costing tool. Proprietary — built for Maneesh. All rights reserved. Not licensed for reuse, redistribution, or resale without permission. */
476"use strict";
477var PDF_URLS = [
478"https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.2/jspdf.umd.min.js",
479"https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.umd.min.js",
480"https://unpkg.com/[email protected]/dist/jspdf.umd.min.js"
481];
482var AT_URLS = [
483"https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.8.2/jspdf.plugin.autotable.min.js",
484"https://cdn.jsdelivr.net/npm/[email protected]/dist/jspdf.plugin.autotable.min.js",
485"https://unpkg.com/[email protected]/dist/jspdf.plugin.autotable.min.js"
486];
487var XLSX_URLS = [
488"https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js",
489"https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js",
490"https://unpkg.com/[email protected]/dist/xlsx.full.min.js"
491];
492function loadScriptCascade(urls, i, cb){
493if(i>=urls.length){ cb(false); return; }
494var s=document.createElement("script");
495s.src=urls[i];
496s.onload=function(){ cb(true); };
497s.onerror=function(){ s.remove(); loadScriptCascade(urls, i+1, cb); };
498document.head.appendChild(s);
499}
500function hasPDF(){ return typeof window.jspdf!=="undefined" && !!window.jspdf.jsPDF; }
501function hasAutoTable(){ return hasPDF() && !!(window.jspdf.jsPDF.API && window.jspdf.jsPDF.API.autoTable); }
502function hasXLSX(){ return typeof window.XLSX!=="undefined"; }
503function ensurePdfLibs(cb){
504if(hasPDF() && hasAutoTable()){ cb(true); return; }
505loadScriptCascade(PDF_URLS, 0, function(ok1){
506if(!ok1 || !hasPDF()){ cb(false); return; }
507if(hasAutoTable()){ cb(true); return; }
508loadScriptCascade(AT_URLS, 0, function(ok2){ cb(ok2 && hasAutoTable()); });
509});
510}
511function ensureXlsx(cb){
512if(hasXLSX()){ cb(true); return; }
513loadScriptCascade(XLSX_URLS, 0, function(ok){ cb(ok && hasXLSX()); });
514}
515ensurePdfLibs(function(){});
516ensureXlsx(function(){});
517var NEEHARA_LOGO = "data:image/jpeg;base64,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";
518var NEEHARA = {
519name: "Neehara Premium IT Solutions",
520addr1: "Kailasam Arcade, Room # 134/D, Opposite Marian College",
521addr2: "Kuttikkanam, Kerala, India, 685531",
522phone: "9400587871",
523gstin: "32AAPFN5644N1ZQ",
524msme: "UDYAM-KL-03-0003042"
525};
526var UNIT = {
527kg:{fam:"weight", f:1000, lbl:"kg"},
528g: {fam:"weight", f:1,    lbl:"g"},
529l: {fam:"volume", f:1000, lbl:"L"},
530ml:{fam:"volume", f:1,    lbl:"ml"},
531nos:{fam:"count", f:1,    lbl:"nos"}
532};
533function famUnits(u){ var fam=(UNIT[u]||UNIT.nos).fam; return Object.keys(UNIT).filter(function(k){return UNIT[k].fam===fam;}); }
534function normUnit(u){
535if(!u) return "";
536var s=String(u).trim().toLowerCase().replace(/[.\s]/g,"");
537var map={kg:"kg",kgs:"kg",kilo:"kg",kilos:"kg",kilogram:"kg",kilograms:"kg",
538g:"g",gm:"g",gms:"g",gram:"g",grams:"g",
539l:"l",lt:"l",ltr:"l",ltrs:"l",litre:"l",litres:"l",liter:"l",liters:"l",
540ml:"ml",millilitre:"ml",milliliter:"ml",
541nos:"nos",no:"nos",pc:"nos",pcs:"nos",piece:"nos",pieces:"nos",each:"nos",unit:"nos",units:"nos",ea:"nos"};
542return map[s]||"";
543}
544var S = {
545settings:{currency:"\u20B9", business:""},
546materials:[], recipes:[], sales:[], batches:[], actuals:{}
547};
548var KEY="ycc.v1";
549var memoryOnly=false;
550function save(){
551try{ localStorage.setItem(KEY, JSON.stringify(S)); }
552catch(e){ memoryOnly=true; }
553updateStatus();
554}
555function load(){
556try{
557var raw=localStorage.getItem(KEY);
558if(raw){ var o=JSON.parse(raw);
559S.settings=Object.assign(S.settings,o.settings||{});
560S.materials=o.materials||[]; S.recipes=o.recipes||[];
561S.sales=o.sales||[]; S.batches=o.batches||[]; S.actuals=o.actuals||{};
562}
563}catch(e){ memoryOnly=true; }
564}
565function updateStatus(){
566var el=document.getElementById("d-status"); if(!el) return;
567el.textContent = memoryOnly
568? "This browser is blocking local storage, so entries will be lost when the page is closed. Download a backup before you leave."
569: "Saved in this browser \u2014 " + S.materials.length + " materials, " + S.recipes.length + " recipes, "
570+ S.sales.length + " sales entries, " + S.batches.length + " batches.";
571}
572function uid(){ return Date.now().toString(36)+Math.random().toString(36).slice(2,7); }
573function $(id){ return document.getElementById(id); }
574function esc(s){ return String(s==null?"":s).replace(/[&<>"']/g,function(c){
575return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]; }); }
576function num(v){
577if(typeof v==="number") return isFinite(v)?v:0;
578var s2=String(v==null?"":v).trim();
579if(/^-?[\d,]+(\.\d+)?$/.test(s2)) s2=s2.replace(/,/g,"");
580var n=parseFloat(s2); return isFinite(n)?n:0;
581}
582function money(v){
583var n=num(v);
584return S.settings.currency + n.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2});
585}
586function qty(v){
587var n=num(v);
588return n.toLocaleString(undefined,{minimumFractionDigits:0,maximumFractionDigits:3});
589}
590function pct(v){ return (num(v)).toFixed(1)+"%"; }
591function toast(msg,isErr){
592var t=$("toast"); t.textContent=msg; t.className="toast"+(isErr?" err":""); t.hidden=false;
593clearTimeout(t._t); t._t=setTimeout(function(){ t.hidden=true; },3400);
594}
595function today(){ return new Date().toISOString().slice(0,10); }
596function getMat(id){ for(var i=0;i<S.materials.length;i++) if(S.materials[i].id===id) return S.materials[i]; return null; }
597function matByCode(code){
598var c=String(code||"").trim().toLowerCase();
599for(var i=0;i<S.materials.length;i++) if(String(S.materials[i].code).trim().toLowerCase()===c) return S.materials[i];
600return null;
601}
602function getRec(id){ for(var i=0;i<S.recipes.length;i++) if(S.recipes[i].id===id) return S.recipes[i]; return null; }
603function recByCode(code){
604var c=String(code||"").trim().toLowerCase();
605for(var i=0;i<S.recipes.length;i++) if(String(S.recipes[i].code).trim().toLowerCase()===c) return S.recipes[i];
606return null;
607}
608function lineQtyInStockUnit(line){
609var m=getMat(line.materialId); if(!m) return 0;
610var lu=UNIT[line.unit]||UNIT[m.unit]||UNIT.nos, mu=UNIT[m.unit]||UNIT.nos;
611if(lu.fam!==mu.fam) return num(line.qty);
612return num(line.qty)*lu.f/mu.f;
613}
614function lineCost(line){
615var m=getMat(line.materialId); if(!m) return 0;
616return lineQtyInStockUnit(line)*num(m.rate);
617}
618function recipeCost(r){ var t=0; (r.lines||[]).forEach(function(l){ t+=lineCost(l); }); return t; }
619function batchMaterialCost(b){ var t=0; (b.lines||[]).forEach(function(l){ t+=lineCost(l); }); return t; }
620function batchOther(b){ return num(b.labour)+num(b.packaging)+num(b.utilities)+num(b.other); }
621function batchTotal(b){ return batchMaterialCost(b)+batchOther(b); }
622function usageCount(matId){
623var n=0;
624S.recipes.forEach(function(r){ (r.lines||[]).forEach(function(l){ if(l.materialId===matId) n++; }); });
625S.batches.forEach(function(b){ (b.lines||[]).forEach(function(l){ if(l.materialId===matId) n++; }); });
626return n;
627}
628function downloadBlob(name, blob){
629var a=document.createElement("a"), url=URL.createObjectURL(blob);
630a.href=url; a.download=name; document.body.appendChild(a); a.click();
631setTimeout(function(){ URL.revokeObjectURL(url); a.remove(); },300);
632}
633function emptyRow(cols,title,msg){
634return '<tr><td colspan="'+cols+'"><div class="empty"><strong>'+esc(title)+'</strong>'+esc(msg)+'</div></td></tr>';
635}
636function pdfOwnerPassword(){
637return "neehara-" + (S.settings.business||"report").toLowerCase().replace(/[^a-z0-9]/g,"").slice(0,16) + "-2026";
638}
639function exportPdf(title, subtitle, head, body, filename){
640if(hasPDF() && hasAutoTable()){ doExportPdf(title, subtitle, head, body, filename); return; }
641toast("Connecting to the PDF component\u2026");
642ensurePdfLibs(function(ok){
643if(!ok){ toast("Couldn't reach the PDF component. Check your internet connection and try again.",true); return; }
644doExportPdf(title, subtitle, head, body, filename);
645});
646}
647function doExportPdf(title, subtitle, head, body, filename){
648var doc;
649try{
650doc = new window.jspdf.jsPDF({
651orientation: head.length>6 ? "landscape" : "portrait",
652unit:"mm", format:"a4",
653encryption:{ userPassword:"", ownerPassword: pdfOwnerPassword(),
654userPermissions:["print"] }
655});
656}catch(e){ doc = new window.jspdf.jsPDF({ orientation: head.length>6 ? "landscape":"portrait", unit:"mm", format:"a4" }); }
657var pw = doc.internal.pageSize.getWidth(), ph = doc.internal.pageSize.getHeight();
658var mL = 12, mR = pw-12, footTop = ph-19;
659doc.setFont("helvetica","bold"); doc.setFontSize(14); doc.setTextColor(16,22,25);
660doc.text(S.settings.business || "Consumption & Product Costing", mL, 16);
661doc.setFont("helvetica","normal"); doc.setFontSize(10); doc.setTextColor(74,88,95);
662doc.text(title, mL, 22.5);
663doc.setFontSize(8.5); doc.setTextColor(125,138,144);
664doc.text("Generated "+today()+(subtitle?"  \u00B7  "+subtitle:""), mL, 27.5);
665doc.setDrawColor(211,218,222); doc.setLineWidth(0.3); doc.line(mL,30,mR,30);
666doc.autoTable({
667head:[head], body:body, startY:34, margin:{left:mL,right:12,bottom:24},
668styles:{font:"helvetica",fontSize:8.5,cellPadding:2.2,textColor:[16,22,25],lineColor:[230,234,236],lineWidth:0.15},
669headStyles:{fillColor:[16,22,25],textColor:255,fontStyle:"bold",fontSize:8},
670alternateRowStyles:{fillColor:[247,249,249]},
671columnStyles:{0:{fontStyle:"bold"}},
672didDrawPage: function(){ stampFooter(doc, mL, mR, footTop, ph); }
673});
674var n = doc.internal.getNumberOfPages();
675for(var i=1;i<=n;i++){ doc.setPage(i); stampPageNum(doc, mR, ph, i, n); }
676doc.save(filename);
677toast("PDF downloaded.");
678}
679function stampFooter(doc, mL, mR, footTop, ph){
680doc.setDrawColor(16,22,25); doc.setLineWidth(0.5); doc.line(mL, footTop, mR, footTop);
681var logoW=22, logoH=logoW*79/401, logoY=footTop+2.6;
682try{ doc.addImage(NEEHARA_LOGO,"JPEG",mL,logoY,logoW,logoH); }catch(e){}
683var tx=mL+logoW+3;
684doc.setFont("helvetica","bold"); doc.setFontSize(7.5); doc.setTextColor(16,22,25);
685doc.text("Powered by "+NEEHARA.name, tx, footTop+5.2);
686doc.setFont("helvetica","normal"); doc.setFontSize(6.6); doc.setTextColor(120,132,138);
687doc.text(NEEHARA.addr1+", "+NEEHARA.addr2, tx, footTop+8.6);
688doc.text("Phone "+NEEHARA.phone+"  \u00B7  GSTIN "+NEEHARA.gstin+"  \u00B7  MSME/Udyam "+NEEHARA.msme, tx, footTop+11.8);
689}
690function stampPageNum(doc, mR, ph, i, n){
691doc.setFont("helvetica","normal"); doc.setFontSize(7.5); doc.setTextColor(120,132,138);
692doc.text("Page "+i+" of "+n, mR, ph-6, {align:"right"});
693}
694var tabs=document.querySelectorAll(".tab");
695tabs.forEach(function(t){
696t.addEventListener("click",function(){
697tabs.forEach(function(x){ x.setAttribute("aria-selected", x===t ? "true":"false"); });
698document.querySelectorAll(".page").forEach(function(p){ p.hidden=true; });
699$("p-"+t.dataset.tab).hidden=false;
700if(t.dataset.tab==="reports") renderReports();
701window.scrollTo({top:0,behavior:"instant"});
702});
703});
704function counts(){
705$("c-mat").textContent=S.materials.length; $("c-rec").textContent=S.recipes.length;
706$("c-sal").textContent=S.sales.length; $("c-bat").textContent=S.batches.length;
707}
708function renderMaterials(){
709var q=($("m-search").value||"").trim().toLowerCase();
710var list=S.materials.filter(function(m){
711return !q || (m.name+" "+m.code+" "+(m.category||"")).toLowerCase().indexOf(q)>=0;
712});
713var tb=$("m-table").querySelector("tbody");
714if(!list.length){
715tb.innerHTML=emptyRow(7, S.materials.length? "Nothing matches that search":"Start with your purchase list",
716S.materials.length? "Try a shorter word." : "Add the raw materials you buy, with the rate you buy them at. Recipes and batches are priced from here.");
717} else {
718tb.innerHTML=list.map(function(m){
719var used=usageCount(m.id);
720return '<tr>'+
721'<td class="mono">'+esc(m.code)+'</td>'+
722'<td class="tname">'+esc(m.name)+'</td>'+
723'<td>'+esc(m.category||"\u2014")+'</td>'+
724'<td>'+esc(UNIT[m.unit]?UNIT[m.unit].lbl:m.unit)+'</td>'+
725'<td class="n">'+money(m.rate)+'</td>'+
726'<td class="n">'+(used?used+' place'+(used>1?'s':''):'\u2014')+'</td>'+
727'<td class="n"><button class="linkbtn" data-medit="'+m.id+'">Edit</button> '+
728'<button class="linkbtn" data-mdel="'+m.id+'" style="color:var(--red)">Delete</button></td></tr>';
729}).join("");
730}
731$("m-count").textContent=S.materials.length+" material"+(S.materials.length===1?"":"s")+" on file";
732var cats={};
732 S.materials.forEach(function(m){ if(m.category) cats[m.category]=1; });
733$("catlist").innerHTML=Object.keys(cats).map(function(c){ return '<option value="'+esc(c)+'">'; }).join("");
734counts();
735}
736$("m-add").addEventListener("click",function(){
737var name=$("m-name").value.trim();
738if(!name){ toast("Give the material a name.",true); $("m-name").focus(); return; }
739var code=$("m-code").value.trim() || ("RM"+String(S.materials.length+1).padStart(3,"0"));
740if(matByCode(code)){ toast("Code "+code+" is already used.",true); return; }
741S.materials.push({id:uid(),code:code,name:name,category:$("m-cat").value.trim(),
742unit:$("m-unit").value,rate:num($("m-rate").value)});
743["m-code","m-name","m-rate"].forEach(function(i){ $(i).value=""; });
744save(); renderMaterials(); refreshMaterialPickers(); $("m-name").focus();
745toast(name+" added.");
746});
747$("m-search").addEventListener("input",renderMaterials);
748$("m-table").addEventListener("click",function(e){
749var ed=e.target.dataset.medit, dl=e.target.dataset.mdel;
750if(ed){
751var m=getMat(ed); if(!m) return;
752var nn=prompt("Material name",m.name); if(nn===null) return;
753var nr=prompt("Rate per "+(UNIT[m.unit]?UNIT[m.unit].lbl:m.unit),m.rate); if(nr===null) return;
754m.name=nn.trim()||m.name; m.rate=num(nr);
755save(); renderMaterials(); refreshMaterialPickers(); toast("Material updated.");
756}
757if(dl){
758var mm=getMat(dl); if(!mm) return;
759var used=usageCount(dl);
760if(used && !confirm(mm.name+" is used in "+used+" recipe/batch line(s). Delete anyway?")) return;
761if(!used && !confirm("Delete "+mm.name+"?")) return;
762S.materials=S.materials.filter(function(x){ return x.id!==dl; });
763save(); renderMaterials(); refreshMaterialPickers(); toast("Deleted.");
764}
765});
766$("m-csv").addEventListener("click",function(){
767var head=["Code","Name","Category","Unit","Rate"];
768var body=S.materials.map(function(m){ return [m.code,m.name,m.category||"\u2014",UNIT[m.unit]?UNIT[m.unit].lbl:m.unit,money(m.rate)]; });
769exportPdf("Raw material list", S.materials.length+" materials", head, body, "raw-materials.pdf");
770});
771var RM=null;
772function matOptions(sel){
773return '<option value="">Choose material\u2026</option>'+S.materials.map(function(m){
774return '<option value="'+m.id+'"'+(m.id===sel?" selected":"")+'>'+esc(m.code+" \u2014 "+m.name)+'</option>';
775}).join("");
776}
777function unitOptions(mat,sel){
778var list = mat ? famUnits(mat.unit) : Object.keys(UNIT);
779return list.map(function(u){ return '<option value="'+u+'"'+(u===sel?" selected":"")+'>'+UNIT[u].lbl+'</option>'; }).join("");
780}
781function renderLineTable(tableId, obj){
782var tb=$(tableId).querySelector("tbody"), tf=$(tableId).querySelector("tfoot");
783if(!obj.lines.length){
784tb.innerHTML='<tr><td colspan="5"><div class="empty" style="padding:22px"><strong>No ingredients yet</strong>Add one line for each raw material.</div></td></tr>';
785tf.innerHTML=""; return;
786}
787tb.innerHTML=obj.lines.map(function(l,i){
788var m=getMat(l.materialId);
789return '<tr>'+
790'<td><select data-i="'+i+'" data-k="materialId">'+matOptions(l.materialId)+'</select></td>'+
791'<td><input type="number" step="0.001" min="0" data-i="'+i+'" data-k="qty" value="'+(l.qty===0||l.qty?l.qty:"")+'"></td>'+
792'<td><select data-i="'+i+'" data-k="unit">'+unitOptions(m,l.unit)+'</select></td>'+
793'<td class="n">'+money(lineCost(l))+'</td>'+
794'<td class="n"><button class="linkbtn" data-del="'+i+'" style="color:var(--red)">&times;</button></td></tr>';
795}).join("");
796var tot=0; obj.lines.forEach(function(l){ tot+=lineCost(l); });
797tf.innerHTML='<tr><td colspan="3">Total</td><td class="n">'+money(tot)+'</td><td></td></tr>';
798}
799function wireLineTable(tableId, obj, after){
800var t=$(tableId);
801t.addEventListener("change",function(e){
802var i=e.target.dataset.i, k=e.target.dataset.k; if(i===undefined||!k) return;
803i=+i;
804if(k==="qty") obj.lines[i].qty=num(e.target.value);
805else obj.lines[i][k]=e.target.value;
806if(k==="materialId"){ var m=getMat(e.target.value); obj.lines[i].unit = m ? m.unit : ""; }
807renderLineTable(tableId,obj); if(after) after();
808});
809t.addEventListener("input",function(e){
810if(e.target.dataset.k==="qty"){ obj.lines[+e.target.dataset.i].qty=num(e.target.value);
811var tot=0;
811 obj.lines.forEach(function(l){ tot+=lineCost(l); });
812var tf=$(tableId).querySelector("tfoot tr");
813if(tf) tf.children[1].textContent=money(tot);
814if(after) after();
815}
816});
817t.addEventListener("click",function(e){
818var d=e.target.dataset.del; if(d===undefined) return;
819obj.lines.splice(+d,1); renderLineTable(tableId,obj); if(after) after();
820});
821}
822function openRecipe(id){
823if(!S.materials.length){ toast("Add a few raw materials first.",true); return; }
824var r=id?getRec(id):null;
825RM = r ? JSON.parse(JSON.stringify(r)) : {id:uid(),code:"",name:"",price:0,lines:[]};
826$("rm-title").textContent = r ? "Edit recipe" : "New recipe";
827$("rm-code").value=RM.code; $("rm-name").value=RM.name; $("rm-price").value=RM.price||"";
828renderLineTable("rm-lines",RM);
829$("ovl-r").hidden=false; $("rm-name").focus();
830}
831$("r-new").addEventListener("click",function(){ openRecipe(null); });
832$("rm-addline").addEventListener("click",function(){
833var m=S.materials[0];
834RM.lines.push({materialId:"",qty:0,unit:m?m.unit:"g"});
835renderLineTable("rm-lines",RM);
836});
837wireLineTable("rm-lines", {get lines(){return RM.lines;}});
838$("rm-save").addEventListener("click",function(){
839RM.name=$("rm-name").value.trim();
840if(!RM.name){ toast("Give the menu item a name.",true); return; }
841RM.code=$("rm-code").value.trim() || ("M"+String(S.recipes.length+1).padStart(3,"0"));
842RM.price=num($("rm-price").value);
843RM.lines=RM.lines.filter(function(l){ return l.materialId; });
844var ex=recByCode(RM.code);
845if(ex && ex.id!==RM.id){ toast("Item code "+RM.code+" is already used.",true); return; }
846var i=S.recipes.findIndex(function(x){ return x.id===RM.id; });
847if(i>=0) S.recipes[i]=RM; else S.recipes.push(RM);
848save(); renderRecipes(); refreshMaterialPickers(); $("ovl-r").hidden=true;
849toast(RM.name+" saved.");
850});
851function renderRecipes(){
852var tb=$("r-table").querySelector("tbody");
853if(!S.recipes.length){
854tb.innerHTML=emptyRow(7,"No recipes yet","A recipe links a menu item to the raw materials one portion uses. Once they exist, sales figures turn into consumption automatically.");
855} else {
856tb.innerHTML=S.recipes.map(function(r){
857var c=recipeCost(r), fc = r.price>0 ? c/r.price*100 : 0;
858var cls = !r.price ? "" : fc>40 ? "bad" : fc>32 ? "warn" : "ok";
859return '<tr>'+
860'<td class="mono">'+esc(r.code)+'</td>'+
861'<td class="tname">'+esc(r.name)+'</td>'+
862'<td class="n">'+(r.lines||[]).length+'</td>'+
863'<td class="n">'+money(c)+'</td>'+
864'<td class="n">'+(r.price?money(r.price):"\u2014")+'</td>'+
865'<td class="n">'+(r.price?'<span class="tag '+cls+'">'+pct(fc)+'</span>':"\u2014")+'</td>'+
866'<td class="n"><button class="linkbtn" data-redit="'+r.id+'">Edit</button> '+
867'<button class="linkbtn" data-rdel="'+r.id+'" style="color:var(--red)">Delete</button></td></tr>';
868}).join("");
869}
870counts();
871}
872$("r-table").addEventListener("click",function(e){
873if(e.target.dataset.redit) openRecipe(e.target.dataset.redit);
874if(e.target.dataset.rdel){
875var r=getRec(e.target.dataset.rdel);
876if(r && confirm("Delete recipe "+r.name+"? Sales entries for it will also be removed.")){
877S.recipes=S.recipes.filter(function(x){ return x.id!==r.id; });
878S.sales=S.sales.filter(function(s){ return s.recipeId!==r.id; });
879save(); renderRecipes(); renderSales(); refreshMaterialPickers(); toast("Recipe deleted.");
880}
881}
882});
883function refreshMaterialPickers(){
884var sel=$("s-item"), cur=sel.value;
885sel.innerHTML = S.recipes.length
886? S.recipes.map(function(r){ return '<option value="'+r.id+'">'+esc(r.code+" \u2014 "+r.name)+'</option>'; }).join("")
887: '<option value="">Add a recipe first</option>';
888if(cur) sel.value=cur;
889renderMaterials(); renderRecipes(); renderBatches();
890}
891$("s-add").addEventListener("click",function(){
892var rid=$("s-item").value;
893if(!rid){ toast("Create a recipe before recording sales.",true); return; }
894var q=num($("s-qty").value);
895if(q<=0){ toast("Enter how many were sold.",true); $("s-qty").focus(); return; }
896S.sales.push({id:uid(),date:$("s-date").value||today(),recipeId:rid,qty:q});
897$("s-qty").value=""; save(); renderSales(); $("s-qty").focus();
898toast("Sale recorded.");
899});
900function renderSales(){
901var tb=$("s-table").querySelector("tbody"), tf=$("s-table").querySelector("tfoot");
902var list=S.sales.slice().sort(function(a,b){ return (b.date||"").localeCompare(a.date||""); });
903if(!list.length){
904tb.innerHTML=emptyRow(6,"No sales recorded","Add the quantity sold for each menu item, or import a month of sales from Excel.");
905tf.innerHTML="";
906} else {
907tb.innerHTML=list.map(function(s){
908var r=getRec(s.recipeId); if(!r) return "";
909return '<tr>'+
910'<td class="mono">'+esc(s.date)+'</td>'+
911'<td class="tname">'+esc(r.name)+'</td>'+
912'<td class="n">'+qty(s.qty)+'</td>'+
913'<td class="n">'+money(s.qty*num(r.price))+'</td>'+
914'<td class="n">'+money(s.qty*recipeCost(r))+'</td>'+
915'<td class="n"><button class="linkbtn" data-sdel="'+s.id+'" style="color:var(--red)">Remove</button></td></tr>';
916}).join("");
917var rev=0,cost=0,units=0;
918list.forEach(function(s){ var r=getRec(s.recipeId); if(!r) return;
919rev+=s.qty*num(r.price); cost+=s.qty*recipeCost(r); units+=s.qty; });
920tf.innerHTML='<tr><td colspan="2">Total</td><td class="n">'+qty(units)+'</td><td class="n">'+money(rev)+'</td><td class="n">'+money(cost)+'</td><td></td></tr>';
921}
922$("s-count").textContent=S.sales.length+" entr"+(S.sales.length===1?"y":"ies");
923counts();
924}
925$("s-table").addEventListener("click",function(e){
926var d=e.target.dataset.sdel; if(!d) return;
927S.sales=S.sales.filter(function(x){ return x.id!==d; });
928save(); renderSales();
929});
930$("s-clear").addEventListener("click",function(){
931if(!S.sales.length) return;
932if(confirm("Remove all "+S.sales.length+" sales entries? Recipes and materials are kept.")){
933S.sales=[]; save(); renderSales(); toast("Sales cleared.");
934}
935});
936$("s-csv").addEventListener("click",function(){
937var head=["Date","Item Code","Item Name","Qty Sold","Revenue","Recipe Cost"];
938var list=S.sales.slice().sort(function(a,b){ return (b.date||"").localeCompare(a.date||""); });
939var body=[]; list.forEach(function(s){ var r=getRec(s.recipeId); if(!r) return;
940body.push([s.date,r.code,r.name,qty(s.qty),money(s.qty*num(r.price)),money(s.qty*recipeCost(r))]); });
941exportPdf("Sales entries", S.sales.length+" entries", head, body, "sales.pdf");
942});
943var BM=null;
944function openBatch(id){
945if(!S.materials.length){ toast("Add a few raw materials first.",true); return; }
946var b=id?S.batches.filter(function(x){return x.id===id;})[0]:null;
947BM = b ? JSON.parse(JSON.stringify(b))
948: {id:uid(),batchNo:"",date:today(),product:"",outQty:0,outUnit:"packs",
949lines:[],labour:0,packaging:0,utilities:0,other:0};
950$("bm-title").textContent = b ? "Edit batch" : "New production batch";
951$("bm-no").value=BM.batchNo; $("bm-date").value=BM.date; $("bm-prod").value=BM.product;
952$("bm-qty").value=BM.outQty||""; $("bm-unit").value=BM.outUnit;
953$("bm-lab").value=BM.labour||""; $("bm-pak").value=BM.packaging||"";
954$("bm-utl").value=BM.utilities||""; $("bm-oth").value=BM.other||"";
955renderLineTable("bm-lines",BM); bmNote();
956$("ovl-b").hidden=false; $("bm-prod").focus();
957}
958function bmNote(){
959var mat=batchMaterialCost(BM);
960var oth=num($("bm-lab").value)+num($("bm-pak").value)+num($("bm-utl").value)+num($("bm-oth").value);
961var q=num($("bm-qty").value);
962$("bm-note").innerHTML = q>0
963? "Cost per "+esc($("bm-unit").value||"unit")+": <strong class=\"mono\">"+money((mat+oth)/q)+
964"</strong> &nbsp;\u00B7&nbsp; batch total "+money(mat+oth)
965: "Enter the output quantity to see the cost per unit.";
966}
967["bm-qty","bm-unit","bm-lab","bm-pak","bm-utl","bm-oth"].forEach(function(i){
968$(i).addEventListener("input",bmNote);
969});
970$("b-new").addEventListener("click",function(){ openBatch(null); });
971$("bm-addline").addEventListener("click",function(){
972var m=S.materials[0];
973BM.lines.push({materialId:"",qty:0,unit:m?m.unit:"kg"});
974renderLineTable("bm-lines",BM);
975});
976wireLineTable("bm-lines", {get lines(){return BM.lines;}}, bmNote);
977$("bm-save").addEventListener("click",function(){
978BM.product=$("bm-prod").value.trim();
979if(!BM.product){ toast("Name the finished good.",true); return; }
980BM.outQty=num($("bm-qty").value);
981if(BM.outQty<=0){ toast("Enter how much finished good came out.",true); $("bm-qty").focus(); return; }
982BM.batchNo=$("bm-no").value.trim() || ("B"+String(S.batches.length+1).padStart(3,"0"));
983BM.date=$("bm-date").value||today();
984BM.outUnit=$("bm-unit").value.trim()||"units";
985BM.labour=num($("bm-lab").value); BM.packaging=num($("bm-pak").value);
986BM.utilities=num($("bm-utl").value); BM.other=num($("bm-oth").value);
987BM.lines=BM.lines.filter(function(l){ return l.materialId; });
988var i=S.batches.findIndex(function(x){ return x.id===BM.id; });
989if(i>=0) S.batches[i]=BM; else S.batches.push(BM);
990save(); renderBatches(); $("ovl-b").hidden=true; toast("Batch "+BM.batchNo+" saved.");
991});
992function renderBatches(){
993var tb=$("b-table").querySelector("tbody");
994var list=S.batches.slice().sort(function(a,b){ return (b.date||"").localeCompare(a.date||""); });
995if(!list.length){
996tb.innerHTML=emptyRow(9,"No production batches yet","Record one run: what went in, what came out. The cost of a single pack falls out of it.");
997$("b-detail").innerHTML=""; counts(); return;
998}
999tb.innerHTML=list.map(function(b){
1000var mc=batchMaterialCost(b), oc=batchOther(b), tc=mc+oc;
1001return '<tr>'+
1002'<td class="mono">'+esc(b.batchNo)+'</td>'+
1003'<td class="mono">'+esc(b.date)+'</td>'+
1004'<td class="tname">'+esc(b.product)+'</td>'+
1005'<td class="n">'+qty(b.outQty)+' '+esc(b.outUnit)+'</td>'+
1006'<td class="n">'+money(mc)+'</td>'+
1007'<td class="n">'+money(oc)+'</td>'+
1008'<td class="n">'+money(tc)+'</td>'+
1009'<td class="n"><strong>'+money(b.outQty>0?tc/b.outQty:0)+'</strong></td>'+
1010'<td class="n"><button class="linkbtn" data-bedit="'+b.id+'">Edit</button> '+
1011'<button class="linkbtn" data-bdel="'+b.id+'" style="color:var(--red)">Delete</button></td></tr>';
1012}).join("");
1013renderBatchDetail(list[0]);
1014counts();
1015}
1016var LADDER=["#0d5b4e","#118a75","#3fa896","#7cc4b7","#b0ddd4","#8f5a0c","#c08a3e","#d9b072"];
1017function renderBatchDetail(b){
1018if(!b){ $("b-detail").innerHTML=""; return; }
1019var tc=batchTotal(b); if(tc<=0){ $("b-detail").innerHTML=""; return; }
1020var parts=(b.lines||[]).map(function(l){
1021var m=getMat(l.materialId);
1022return {name:m?m.name:"Unknown", v:lineCost(l), q:lineQtyInStockUnit(l), u:m?(UNIT[m.unit]?UNIT[m.unit].lbl:m.unit):""};
1023}).filter(function(p){ return p.v>0; }).sort(function(a,b2){ return b2.v-a.v; });
1024[["Labour",b.labour],["Packaging",b.packaging],["Power & fuel",b.utilities],["Other overheads",b.other]]
1025.forEach(function(o){ if(num(o[1])>0) parts.push({name:o[0],v:num(o[1]),q:null}); });
1026var bars=parts.map(function(p,i){
1027return '<span style="width:'+(p.v/tc*100)+'%;background:'+LADDER[i%LADDER.length]+'" title="'+esc(p.name)+'"></span>';
1028}).join("");
1029var key=parts.map(function(p,i){
1030return '<span><span class="swatch" style="background:'+LADDER[i%LADDER.length]+'"></span>'+
1031esc(p.name)+' &middot; <span class="mono">'+money(p.v)+'</span> <span class="tsub">('+pct(p.v/tc*100)+')</span></span>';
1032}).join("");
1033var rows=parts.map(function(p){
1034return '<tr><td class="tname">'+esc(p.name)+'</td>'+
1035'<td class="n">
1035'+(p.q!=null?qty(p.q)+" "+esc(p.u):"\u2014")+'</td>'+
1036'<td class="n">'+money(p.v)+'</td>'+
1037'<td class="n">'+pct(p.v/tc*100)+'</td>'+
1038'<td class="n">'+money(b.outQty>0?p.v/b.outQty:0)+'</td></tr>';
1039}).join("");
1040$("b-detail").innerHTML=
1041'<div class="panel"><div class="panel-hd"><div><h2>Cost build-up \u2014 '+esc(b.product)+'</h2>'+
1042'<div class="sub">Batch '+esc(b.batchNo)+' \u00B7 '+esc(b.date)+' \u00B7 output '+qty(b.outQty)+' '+esc(b.outUnit)+
1043' \u00B7 <strong>'+money(b.outQty>0?tc/b.outQty:0)+' per '+esc(b.outUnit.replace(/s$/,""))+'</strong></div></div>'+
1044'<select id="b-pick" style="width:auto">'+S.batches.map(function(x){
1045return '<option value="'+x.id+'"'+(x.id===b.id?" selected":"")+'>'+esc(x.batchNo+" \u2014 "+x.product)+'</option>';
1046}).join("")+'</select></div>'+
1047'<div class="panel-bd"><div class="ladder">'+bars+'</div><div class="ladder-key">'+key+'</div></div>'+
1048'<div class="panel-bd tight"><div class="tscroll"><table>'+
1049'<thead><tr><th>Cost element</th><th class="n">Quantity</th><th class="n">Value</th><th class="n">Share</th><th class="n">Per '+esc(b.outUnit.replace(/s$/,""))+'</th></tr></thead>'+
1050'<tbody>'+rows+'</tbody>'+
1051'<tfoot><tr><td>Total batch cost</td><td class="n">\u2014</td><td class="n">'+money(tc)+'</td><td class="n">100.0%</td>'+
1052'<td class="n">'+money(b.outQty>0?tc/b.outQty:0)+'</td></tr></tfoot></table></div></div></div>';
1053$("b-pick").addEventListener("change",function(){
1054renderBatchDetail(S.batches.filter(function(x){ return x.id===this.value; })[0]);
1055});
1056}
1057$("b-table").addEventListener("click",function(e){
1058if(e.target.dataset.bedit) openBatch(e.target.dataset.bedit);
1059if(e.target.dataset.bdel){
1060var id=e.target.dataset.bdel;
1061if(confirm("Delete this batch?")){
1062S.batches=S.batches.filter(function(x){ return x.id!==id; });
1063save(); renderBatches(); toast("Batch deleted.");
1064}
1065}
1066});
1067function filteredSales(){
1068var f=$("f-from").value, t=$("f-to").value;
1069return S.sales.filter(function(s){
1070if(f && (s.date||"")<f) return false;
1071if(t && (s.date||"")>t) return false;
1072return true;
1073});
1074}
1075function renderReports(){
1076var sales=filteredSales();
1077var agg={};
1078sales.forEach(function(s){
1079var r=getRec(s.recipeId); if(!r) return;
1080if(!agg[r.id]) agg[r.id]={r:r,q:0};
1081agg[r.id].q+=num(s.qty);
1082});
1083var rows=Object.keys(agg).map(function(k){ return agg[k]; })
1084.sort(function(a,b){ return b.q*num(b.r.price)-a.q*num(a.r.price); });
1085var tRev=0,tCost=0,tQty=0;
1086var tb=$("rp-table").querySelector("tbody"), tf=$("rp-table").querySelector("tfoot");
1087if(!rows.length){
1088tb.innerHTML=emptyRow(8,"Nothing to report yet","Record some sales against your recipes, then come back here.");
1089tf.innerHTML="";
1090} else {
1091tb.innerHTML=rows.map(function(x){
1092var cpp=recipeCost(x.r), cost=cpp*x.q, rev=num(x.r.price)*x.q;
1093tRev+=rev; tCost+=cost; tQty+=x.q;
1094var fc=rev>0?cost/rev*100:0;
1095var cls=!rev?"":fc>40?"bad":fc>32?"warn":"ok";
1096return '<tr><td class="tname">'+esc(x.r.name)+'<div class="tsub mono">'+esc(x.r.code)+'</div></td>'+
1097'<td class="n">'+qty(x.q)+'</td><td class="n">'+money(cpp)+'</td>'+
1098'<td class="n">'+money(x.r.price)+'</td><td class="n">'+money(cost)+'</td>'+
1099'<td class="n">'+money(rev)+'</td><td class="n">'+money(rev-cost)+'</td>'+
1100'<td class="n">'+(rev?'<span class="tag '+cls+'">'+pct(fc)+'</span>':"\u2014")+'</td></tr>';
1101}).join("");
1102tf.innerHTML='<tr><td>Total</td><td class="n">'+qty(tQty)+'</td><td class="n">\u2014</td><td class="n">\u2014</td>'+
1103'<td class="n">'+money(tCost)+'</td><td class="n">'+money(tRev)+'</td><td class="n">'+money(tRev-tCost)+'</td>'+
1104'<td class="n">'+(tRev?pct(tCost/tRev*100):"\u2014")+'</td></tr>';
1105}
1106var cons={};
1107sales.forEach(function(s){
1108var r=getRec(s.recipeId); if(!r) return;
1109(r.lines||[]).forEach(function(l){
1110if(!l.materialId) return;
1111cons[l.materialId]=(cons[l.materialId]||0)+lineQtyInStockUnit(l)*num(s.qty);
1112});
1113});
1114var cb=$("rc-table").querySelector("tbody"), cf=$("rc-table").querySelector("tfoot");
1115var ids=Object.keys(cons).sort(function(a,b){
1116return cons[b]*num((getMat(b)||{}).rate)-cons[a]*num((getMat(a)||{}).rate); });
1117var thV=0, acV=0, hasActual=false;
1118if(!ids.length){
1119cb.innerHTML=emptyRow(8,"No consumption calculated","This fills in once recipes have ingredients and sales have been recorded.");
1120cf.innerHTML="";
1121} else {
1122cb.innerHTML=ids.map(function(id){
1123var m=getMat(id); if(!m) return "";
1124var th=cons[id], thv=th*num(m.rate);
1125var ac=S.actuals[id]; var hasA=(ac!==undefined && ac!==null && ac!=="");
1126if(hasA) hasActual=true;
1127var acn=num(ac), acv=acn*num(m.rate);
1128thV+=thv; if(hasA) acV+=acv;
1129var vq=hasA?acn-th:0, vv=hasA?acv-thv:0;
1130var vp=hasA&&th>0?vq/th*100:0;
1131var cls=!hasA?"":vp>5?"bad":vp>2?"warn":"ok";
1132var scale=Math.max(th,hasA?acn:0)||1;
1133var meter='<div class="meter"><div class="theo" style="width:'+(th/scale*100)+'%"></div>'+
1134(hasA?'<div class="act" style="width:'+(acn/scale*100)+'%"></div>':"")+
1135(hasA&&acn>th?'<div class="over" style="left:'+(th/scale*100)+'%;width:'+((acn-th)/scale*100)+'%"></div>':"")+'</div>';
1136return '<tr><td class="tname">'+esc(m.name)+'<div class="tsub mono">'+esc(m.code)+'</div></td>'+
1137'<td>'+esc(UNIT[m.unit]?UNIT[m.unit].lbl:m.unit)+'</td>'+
1138'<td class="n">'+qty(th)+'</td><td class="n">'+money(thv)+'</td>'+
1139'<td class="n"><input type="number" step="0.001" min="0" style="width:96px" data-act="'+id+'" value="'+(hasA?ac:"")+'" placeholder="\u2014"></td>'+
1140'<td class="n">'+(hasA?(vq>0?"+":"")+qty(vq)+' <span class="tag '+cls+'">'+(vq>0?"+":"")+pct(vp)+'</span>':"\u2014")+'</td>'+
1141'<td class="n">'+(hasA?(vv>0?"+":"")+money(vv):"\u2014")+'</td>'+
1142'<td>'+meter+'</td></tr>';
1143}).join("");
1144cf.innerHTML='<tr><td colspan="3">Total</td><td class="n">'+money(thV)+'</td><td class="n"></td><td class="n"></td>'+
1145'<td class="n">'+(hasActual?(acV-thV>0?"+":"")+money(acV-thV):"\u2014")+'</td><td></td></tr>';
1146}
1147var fcp=tRev>0?tCost/tRev*100:0;
1148var varV=hasActual?acV-thV:null;
1149$("r-stats").innerHTML=
1150'<div class="stat"><div class="k">
1150Sales revenue</div><div class="v">'+money(tRev)+'</div>'+
1151'<div class="m">'+qty(tQty)+' portions sold</div></div>'+
1152'<div class="stat"><div class="k">Recipe material cost</div><div class="v">'+money(tCost)+'</div>'+
1153'<div class="m">what the recipes say you used</div></div>'+
1154'<div class="stat"><div class="k">Food cost</div><div class="v '+(fcp>40?"bad":fcp>32?"warn":"good")+'">'+(tRev?pct(fcp):"\u2014")+'</div>'+
1155'<div class="m">gross margin '+money(tRev-tCost)+'</div></div>'+
1156'<div class="stat"><div class="k">Variance vs actual</div><div class="v '+(varV==null?"":varV>0?"bad":"good")+'">'+
1157(varV==null?"\u2014":(varV>0?"+":"")+money(varV))+'</div>'+
1158'<div class="m">'+(varV==null?"enter actual issues below":varV>0?"more used than the recipes allow":"less used than the recipes allow")+'</div></div>';
1159var pg={};
1160S.batches.forEach(function(b){
1161var k=b.product||"\u2014";
1162if(!pg[k]) pg[k]={n:0,out:0,unit:b.outUnit,mc:0,oc:0};
1163pg[k].n++; pg[k].out+=num(b.outQty); pg[k].mc+=batchMaterialCost(b); pg[k].oc+=batchOther(b);
1164});
1165var bb=$("rb-table").querySelector("tbody");
1166var keys=Object.keys(pg);
1167bb.innerHTML = keys.length ? keys.map(function(k){
1168var p=pg[k], t=p.mc+p.oc;
1169return '<tr><td class="tname">'+esc(k)+'</td><td class="n">'+p.n+'</td>'+
1170'<td class="n">'+qty(p.out)+' '+esc(p.unit)+'</td><td class="n">'+money(p.mc)+'</td>'+
1171'<td class="n">'+money(p.oc)+'</td><td class="n">'+money(t)+'</td>'+
1172'<td class="n"><strong>'+money(p.out>0?t/p.out:0)+'</strong></td></tr>';
1173}).join("") : emptyRow(7,"No production recorded","Add a batch on the Production tab to cost your finished goods.");
1174}
1175$("rc-table").addEventListener("input",function(e){
1176var id=e.target.dataset.act; if(!id) return;
1177if(e.target.value==="") delete S.actuals[id]; else S.actuals[id]=num(e.target.value);
1178clearTimeout(window._rt); window._rt=setTimeout(save,400);
1179});
1180$("rc-table").addEventListener("change",function(e){
1181if(!e.target.dataset.act) return;
1182save(); renderReports();
1183});
1184["f-from","f-to"].forEach(function(i){ $(i).addEventListener("change",renderReports); });
1185$("f-clear").addEventListener("click",function(){ $("f-from").value=""; $("f-to").value=""; renderReports(); });
1186$("r-print").addEventListener("click",function(){ window.print(); });
1187$("rp-csv").addEventListener("click",function(){
1188var head=["Menu Item","Code","Qty Sold","Cost/Portion","Price","Total Cost","Revenue","Margin","Food Cost %"];
1189var agg={};
1190filteredSales().forEach(function(s){ var r=getRec(s.recipeId); if(!r) return;
1191if(!agg[r.id]) agg[r.id]={r:r,q:0}; agg[r.id].q+=num(s.qty); });
1192var body=Object.keys(agg).map(function(k){
1193var x=agg[k], cpp=recipeCost(x.r), cost=cpp*x.q, rev=num(x.r.price)*x.q;
1194return [x.r.name,x.r.code,qty(x.q),money(cpp),money(x.r.price),money(cost),
1195money(rev),money(rev-cost),rev?pct(cost/rev*100):"\u2014"];
1196});
1197exportPdf("Menu item profitability", $("f-from").value||$("f-to").value ? "period "+($("f-from").value||"\u2026")+" to "+($("f-to").value||"\u2026") : "all recorded sales", head, body, "menu-profitability.pdf");
1198});
1199$("rc-csv").addEventListener("click",function(){
1200var head=["Material","Unit","Theoretical Qty","Theoretical Value","Actual Qty","Variance Qty","Variance Value"];
1201var cons={};
1202filteredSales().forEach(function(s){ var r=getRec(s.recipeId); if(!r) return;
1203(r.lines||[]).forEach(function(l){ if(!l.materialId) return;
1204cons[l.materialId]=(cons[l.materialId]||0)+lineQtyInStockUnit(l)*num(s.qty); }); });
1205var body=Object.keys(cons).map(function(id){
1206var m=getMat(id); if(!m) return null;
1207var th=cons[id], ac=S.actuals[id], hasA=(ac!==undefined&&ac!==null&&ac!=="");
1208return [m.name+" ("+m.code+")",UNIT[m.unit]?UNIT[m.unit].lbl:m.unit,qty(th),money(th*num(m.rate)),
1209hasA?qty(num(ac)):"\u2014", hasA?((num(ac)-th>=0?"+":"")+qty(num(ac)-th)):"\u2014",
1210hasA?((num(ac)-th>=0?"+":"")+money((num(ac)-th)*num(m.rate))):"\u2014"];
1211}).filter(Boolean);
1212exportPdf("Raw material consumption & variance", "theoretical vs. actual issue", head, body, "consumption-variance.pdf");
1213});
1214$("rb-csv").addEventListener("click",function(){
1215var head=["Batch No","Date","Finished Good","Output","Material Cost","Other Cost","Total Cost","Cost/Unit"];
1216var body=S.batches.map(function(b){
1217var mc=batchMaterialCost(b), oc=batchOther(b), t=mc+oc;
1218return [b.batchNo,b.date,b.product,qty(b.outQty)+" "+b.outUnit,money(mc),money(oc),money(t),
1219money(b.outQty>0?t/b.outQty:0)];
1220});
1221exportPdf("Finished goods costing", S.batches.length+" batches", head, body, "finished-goods-costing.pdf");
1222});
1223var TPL={
1224Materials:[["Code","Name","Category","Unit","Rate"],
1225["RM001","Chicken, boneless","Meat","kg",320],
1226["RM002","Basmati rice","Grocery","kg",95],
1227["RM003","Refined oil","Grocery","l",140],
1228["RM004","Raw banana","Vegetable","kg",42],
1229["RM005","Printed pouch 200g","Packing","nos",3.2]],
1230Recipes:[["Item Code","Item Name","Selling Price","Material Code","Qty","Unit"],
1231["M001","Chicken Biryani",320,"RM001",180,"g"],
1232["M001","Chicken Biryani",320,"RM002",150,"g"],
1233["M001","Chicken Biryani",320,"RM003",30,"ml"]],
1234Sales:[["Date","Item Code","Qty Sold"],
1235["2026-08-01","M001",42],
1236["2026-08-02","M001",37]],
1237Production:[["Batch No","Date","Finished Good","Output Qty","Output Unit","Material Code","Qty","Unit","Labour","Packaging","Power & Fuel","Other"],
1238["B001","2026-08-05","Banana chips 200g pack",480,"packs","RM004",140,"kg",2400,0,900,400],
1239["B001","2026-08-05","Banana chips 200g pack",480,"packs","RM003",22,"l","","","",""],
1240["B001","2026-08-05","Banana chips 200g pack",480,"packs","RM005",480,"nos","","","",""]]
1241};
1242$("x-tpl").addEventListener("click",function(){
1243if(hasXLSX()){ writeTemplate(); return; }
1244toast("Connecting to the Excel component\u2026");
1245ensureXlsx(function(ok){
1246if(!ok){ toast("Couldn't reach the Excel component. Check your internet connection and try again.",true); return; }
1247writeTemplate();
1248});
1249});
1250function writeTemplate(){
1251var wb=window.XLSX.utils.book_new();
1252Object.keys(TPL).forEach(function(n){
1253window.XLSX.utils.book_append_sheet(wb, window.XLSX.utils.aoa_to_sheet(TPL[n]), n);
1254});
1255window.XLSX.writeFile(wb,"costing-template.xlsx");
1256toast("Template downloaded. The example rows can be deleted.");
1257}
1258function key(h){ return String(h||"").toLowerCase().replace(/[^a-z0-9]/g,""); }
1259function pick(row,names){
1260for(var i=0;i<names.length;i++){ if(row[names[i]]!==undefined && row[names[i]]!=="") return row[names[i]]; }
1261return "";
1262}
1263function normRow(r){ var o={}; Object.keys(r).forEach(function(k){ o[key(k)]=r[k]; }); return o; }
1264function xdate(v){
1265if(v instanceof Date && !isNaN(v)) return v.toISOString().slice(0,10);
1266var s=String(v||"").trim();
1267if(/^\d{4}-\d{2}-\d{2}/.test(s)) return s.slice(0,10);
1268var m=s.match(/^(\d{1,2})[\/\-.](\d{1,2})[\/\-.](\d{2,4})$/);
1269if(m){ var y=m[3].length===2?"20"+m[3]:m[3];
1270return y+"-"+("0"+m[2]).slice(-2)+"-"+("0"+m[1]).slice(-2); }
1271return s||today();
1272}
1273function logLine(txt,cls){
1274var l=$("x-log"); l.hidden=false;
1275var d=document.createElement("div"); if(cls) d.className=cls; d.textContent=txt; l.appendChild(d); l.scrollTop=l.scrollHeight;
1276}
1277function shapeOf(rows){
1278if(!rows.length) return null;
1279var k=Object.keys(normRow(rows[0]));
1280if(k.indexOf("batchno")>=0||k.indexOf("finishedgood")>=0) return "Production";
1281if(k.indexOf("itemcode")>=0&&k.indexOf("materialcode")>=0) return "Recipes";
1282if(k.indexOf("qtysold")>=0) return "Sales";
1283if(k.indexOf("rate")>=0&&(k.indexOf("code")>=0||k.indexOf("name")>=0)) return "Materials";
1284return null;
1285}
1286function importSheets(sheets){
1287$("x-log").hidden=false;
1288var added={m:0,r:0,s:0,b:0}, errs=0;
1289(sheets.Materials||[]).forEach(function(raw,i){
1290var r=normRow(raw);
1291var name=pick(r,["name","material","materialname","description"]);
1292if(!name) return;
1293var code=String(pick(r,["code","materialcode","itemcode"])||"").trim() || ("RM"+String(S.materials.length+1).padStart(3,"0"));
1294var u=normUnit(pick(r,["unit","uom","purchaseunit"]))||"nos";
1295var rate=num(pick(r,["rate","price","cost","rateperunit","purchaserate"]));
1296var ex=matByCode(code);
1297if(ex){ ex.name=name; ex.unit=u; ex.rate=rate; ex.category=String(pick(r,["category","group"])||ex.category); }
1298else { S.materials.push({id:uid(),code:code,name:String(name),category:String(pick(r,["category","group"])||""),unit:u,rate:rate}); added.m++; }
1299});
1300var rmap={};
1301(sheets.Recipes||[]).forEach(function(raw,i){
1302var r=normRow(raw);
1303var ic=String(pick(r,["itemcode","code","menucode"])||"").trim();
1304var iname=String(pick(r,["itemname","item","menuitem","name","dish"])||"").trim();
1305if(!ic && !iname) return;
1306if(!ic) ic=iname;
1307var rec=rmap[ic];
1308if(!rec){
1309rec=recByCode(ic);
1310if(rec){ rec.lines=[]; }
1311else { rec={id:uid(),code:ic,name:iname||ic,price:0,lines:[]}; S.recipes.push(rec); added.r++; }
1312if(iname) rec.name=iname;
1313rmap[ic]=rec;
1314}
1315var p=pick(r,["sellingprice","price","rate","mrp"]);
1316if(p!=="") rec.price=num(p);
1317var mc=String(pick(r,["materialcode","rawmaterialcode","rmcode"])||"").trim();
1318if(!mc) return;
1319var m=matByCode(mc);
1320if(!m){ logLine("Recipe "+ic+": material code "+mc+" not found \u2014 line skipped.","bad"); errs++; return; }
1321var u=normUnit(pick(r,["unit","uom"]))||m.unit;
1322if((UNIT[u]||{}).fam!==(UNIT[m.unit]||{}).fam){
1323logLine("Recipe "+ic+" / "+mc+": unit "+u+" does not match "+m.unit+" \u2014 used "+m.unit+".","bad"); errs++; u=m.unit;
1324}
1325rec.lines.push({materialId:m.id,qty:num(pick(r,["qty","quantity","qtyperportion","qtyper1portion"])),unit:u});
1326});
1327(sheets.Sales||[]).forEach(function(raw){
1328var r=normRow(raw);
1329var ic=String(pick(r,["itemcode","code","menucode","item","itemname"])||"").trim();
1330if(!ic) return;
1331var rec=recByCode(ic) || S.recipes.filter(function(x){ return x.name.toLowerCase()===ic.toLowerCase(); })[0];
1332if(!rec){ logLine("Sales: menu item "+ic+" has no recipe \u2014 row skipped.","bad"); errs++; return; }
1333var q=num(pick(r,["qtysold","qty","quantity","sold","nos"]));
1334if(q<=0) return;
1335S.sales.push({id:uid(),date:xdate(pick(r,["date","salesdate","billdate"])),recipeId:rec.id,qty:q});
1336added.s++;
1337});
1338var bmap={};
1339(sheets.Production||[]).forEach(function(raw){
1340var r=normRow(raw);
1341var bn=String(pick(r,["batchno","batch","batchnumber"])||"").trim();
1342var prod=String(pick(r,["finishedgood","product","productname","fg"])||"").trim();
1343if(!bn && !prod) return;
1344if(!bn) bn=prod+"-"+xdate(pick(r,["date"]));
1345var b=bmap[bn];
1346if(!b){
1347b={id:uid(),batchNo:bn,date:xdate(pick(r,["date","productiondate"])),product:prod||bn,
1348outQty:num(pick(r,["outputqty","output","outputquantity","finishedqty","qtyproduced"])),
1349outUnit:String(pick(r,["outputunit","fgunit","outunit"])||"units"),
1350lines:[],labour:num(pick(r,["labour","labor","labourcost"])),
1351packaging:num(pick(r,["packaging","packagingcost","packing"])),
1352utilities:num(pick(r,["powerfuel","power","fuel","utilities","electricity"])),
1353other:num(pick(r,["other","otheroverheads","overheads","overhead","misc"]))};
1354S.batches.push(b); bmap[bn]=b; added.b++;
1355}
1356var mc=String(pick(r,["materialcode","rawmaterialcode","rmcode"])||"").trim();
1357if(!mc) return;
1358var m=matByCode(mc);
1359if(!m){ logLine("Batch "+bn+": material code "+mc+" not found \u2014 line skipped.","bad"); errs++; return; }
1360var q=num(pick(r,["qty","quantity","qtyused","consumed"]));
1361if(q<=0) return;
1362var u=normUnit(pick(r,["unit","uom"]))||m.unit;
1363if((UNIT[u]||{}).fam!==(UNIT[m.unit]||{}).fam){
1364logLine("Batch "+bn+" / "+mc+": unit "+u+" does not match "+m.unit+" \u2014 used "+m.unit+".","bad"); errs++; u=m.unit;
1365}
1366b.lines.push({materialId:m.id,qty:q,unit:u});
1367});
1368save(); renderMaterials(); renderRecipes(); renderSales(); renderBatches(); refreshMaterialPickers();
1369logLine("Imported: "+added.m+" materials, "+added.r+" recipes, "+added.s+" sales entries, "+added.b+" batches.","ok");
1370if(errs) logLine(errs+" row(s) needed attention \u2014 see the notes above.","bad");
1371toast(errs? "Imported with "+errs+" warning(s).":"Import finished.", !!errs);
1372}
1373function readFile(file){
1374$("x-log").innerHTML=""; $("x-log").hidden=true;
1375if(hasXLSX()){ doReadFile(file); return; }
1376toast("Connecting to the Excel component\u2026");
1377ensureXlsx(function(ok){
1378if(!ok){ toast("Couldn't reach the Excel component. Check your internet connection and try again.",true); return; }
1379doReadFile(file);
1380});
1381}
1382function doReadFile(file){
1383var fr=new FileReader();
1384fr.onload=function(e){
1385try{
1386var wb=window.XLSX.read(new Uint8Array(e.target.result),{type:"array",cellDates:true});
1387var sheets={};
1388wb.SheetNames.forEach(function(n){
1389var rows=window.XLSX.utils.sheet_to_json(wb.Sheets[n],{defval:"",raw:false});
1390var want=["Materials","Recipes","Sales","Production"].filter(function(w){
1391return key(n)===key(w) || key(n).indexOf(key(w))>=0; })[0];
1392if(!want) want=shapeOf(rows);
1393if(want) sheets[want]=(sheets[want]||[]).concat(rows);
1394else if(rows.length) logLine('Sheet "'+n+'" was not recognised \u2014 skipped.',"bad");
1395});
1396if(!Object.keys(sheets).length){ toast("No usable sheets found in that file.",true); return; }
1397importSheets(sheets);
1398}catch(err){ logLine("Could not read the file: "+err.message,"bad"); toast("That file could not be read.",true); }
1399};
1400fr.readAsArrayBuffer(file);
1401}
1402$("x-pick").addEventListener("click",function(){ $("x-file").click(); });
1403$("x-file").addEventListener("change",function(){ if(this.files[0]){ readFile(this.files[0]); this.value=""; } });
1404var drop=$("drop");
1405["dragenter","dragover"].forEach(function(ev){ drop.addEventListener(ev,function(e){ e.preventDefault(); drop.classList.add("drag"); }); });
1406["dragleave","drop"].forEach(function(ev){ drop.addEventListener(ev,function(e){ e.preventDefault(); drop.classList.remove("drag"); }); });
1407drop.addEventListener("drop",function(e){ if(e.dataTransfer.files[0]) readFile(e.dataTransfer.files[0]); });
1408$("cfg-cur").addEventListener("input",function(){
1409S.settings.currency=this.value||"\u20B9"; save(); renderAll();
1410});
1411$("cfg-biz").addEventListener("input",function(){ S.settings.business=this.value; save(); });
1412$("d-export").addEventListener("click",function(){
1413downloadBlob("costing-backup-"+today()+".json", new Blob([JSON.stringify(S,null,2)],{type:"application/json"}));
1414toast("Backup downloaded.");
1415});
1416$("d-import").addEventListener("click",function(){ $("d-file").click(); });
1417$("d-file").addEventListener("change",function(){
1418var f=this.files[0]; if(!f) return;
1419var fr=new FileReader();
1420fr.onload=function(e){
1421try{
1422var o=JSON.parse(e.target.result);
1423if(!o.materials) throw new Error("not a backup file");
1424if(!confirm("Restoring replaces everything currently on this device. Continue?")) return;
1425S.settings=Object.assign({currency:"\u20B9",business:""},o.settings||{});
1426S.materials=o.materials||[]; S.recipes=o.recipes||[]; S.sales=o.sales||[];
1427S.batches=o.batches||[]; S.actuals=o.actuals||{};
1428save(); renderAll(); toast("Backup restored.");
1429}catch(err){ toast("That is not a valid backup file.",true); }
1430};
1431fr.readAsText(f); this.value="";
1432});
1433$("d-wipe").addEventListener("click",function(){
1434if(!confirm("Erase all materials, recipes, sales and batches from this browser?")) return;
1435if(!confirm("This cannot be undone. Do you have a backup?")) return;
1436S.materials=[]; S.recipes=[]; S.sales=[]; S.batches=[]; S.actuals={};
1437save(); renderAll(); toast("Everything erased.");
1438});
1439$("d-demo").addEventListener("click",function(){
1440if(S.materials.length && !confirm("Sample data will be added alongside what you already have. Continue?")) return;
1441var mk=function(code,name,cat,unit,rate){ var m={id:uid(),code:code,name:name,category:cat,unit:unit,rate:rate}; S.materials.push(m); return m; };
1442var chick=mk("RM001","Chicken, boneless","Meat","kg",320),
1443rice =mk("RM002","Basmati rice","Grocery","kg",95),
1444oil  =mk("RM003","Refined oil","Grocery","l",140),
1445onion=mk("RM004","Onion","Vegetable","kg",38),
1446spice=mk("RM005","Biryani masala","Spices","kg",480),
1447curd =mk("RM006","Curd","Dairy","kg",70),
1448banana=mk("RM007","Raw banana","Vegetable","kg",42),
1449salt =mk("RM008","Salt","Grocery","kg",22),
1450pack =mk("RM009","Printed pouch 200g","Packing","nos",3.2);
1451var r1={id:uid(),code:"M001",name:"Chicken Biryani",price:320,lines:[
1452{materialId:chick.id,qty:180,unit:"g"},{materialId:rice.id,qty:150,unit:"g"},
1453{materialId:oil.id,qty:30,unit:"ml"},{materialId:onion.id,qty:60,unit:"g"},
1454{materialId:spice.id,qty:12,unit:"g"},{materialId:curd.id,qty:40,unit:"g"}]};
1455var r2={id:uid(),code:"M002",name:"Veg Fried Rice",price:180,lines:[
1456{materialId:rice.id,qty:160,unit:"g"},{materialId:oil.id,qty:25,unit:"ml"},
1457{materialId:onion.id,qty:70,unit:"g"},{materialId:salt.id,qty:4,unit:"g"}]};
1458S.recipes.push(r1,r2);
1459var d=new Date();
1460for(var i=0;i<7;i++){
1461var dt=new Date(d.getTime()-i*86400000).toISOString().slice(0,10);
1462S.sales.push({id:uid(),date:dt,recipeId:r1.id,qty:30+Math.round(Math.random()*20)});
1463S.sales.push({id:uid(),date:dt,recipeId:r2.id,qty:40+Math.round(Math.random()*25)});
1464}
1465S.batches.push({id:uid(),batchNo:"B001",date:today(),product:"Banana chips 200g pack",
1466outQty:480,outUnit:"packs",
1467lines:[{materialId:banana.id,qty:140,unit:"kg"},{materialId:oil.id,qty:22,unit:"l"},
1468{materialId:salt.id,qty:1.5,unit:"kg"},{materialId:pack.id,qty:480,unit:"nos"}],
1469labour:2400,packaging:0,utilities:900,other:400});
1470save(); renderAll(); toast("Sample data loaded \u2014 open the Reports tab.");
1471});
1472document.querySelectorAll("[data-close]").forEach(function(b){
1473b.addEventListener("click",function(){ $(b.dataset.close).hidden=true; });
1474});
1475document.querySelectorAll(".ovl").forEach(function(o){
1476o.addEventListener("click",function(e){ if(e.target===o) o.hidden=true; });
1477});
1478document.addEventListener("keydown",function(e){
1479if(e.key==="Escape") document.querySelectorAll(".ovl").forEach(function(o){ o.hidden=true; });
1480});
1481function renderAll(){
1482$("cfg-cur").value=S.settings.currency;
1483$("cfg-biz").value=S.settings.business||"";
1484renderMaterials(); renderRecipes(); renderSales(); renderBatches();
1485refreshMaterialPickers(); renderReports(); updateStatus();
1486}
1487load();
1488$("s-date").value=today();
1489renderAll();
1490</script>
1490
1491</body>
1492</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.