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 & 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 & cost control</div> 194 <h1>Consumption & 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 & 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 & 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">×</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">×</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 & 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 {"&":"&","<":"<",">":">",'"':""","'":"'"}[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)">×</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> \u00B7 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)+' · <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.