diff --git a/.gitignore b/.gitignore index 19f7e55..73bf680 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,10 @@ dist next-env.d.ts test-pb.mjs tsconfig.tsbuildinfo + +# next-pwa build artifacts (generated into public/ on every build) +public/sw.js +public/sw.js.map +public/workbox-*.js +public/workbox-*.js.map +public/swe-worker-*.js diff --git a/HANDOFF-PROMPT-2.md b/HANDOFF-PROMPT-2.md new file mode 100644 index 0000000..860ca3c --- /dev/null +++ b/HANDOFF-PROMPT-2.md @@ -0,0 +1,244 @@ +# Mega Plant — Round 2 + +Round 1 (`HANDOFF-PROMPT.md`) was mostly landed well. This round covers three items +that didn't land, plus the asset-register restructure, plus a partial answer to +Open Question 2 that came out of the data. + +## Ground rules (unchanged) + +**DO NOT restyle anything.** Palette, roundness, gradients, spacing, dark mode all +stay exactly as they are. They're approved. + +**Do not invent data to make a step pass.** See B.3 — this happened in round 1. + +**Verify and report.** Run `npm run typecheck` after each part and say what it output. + +--- + +# Part A — Already done for you, do not redo + +The endless-recompile loop is **fixed**. Root cause: `next-pwa` writes `sw.js` and +`workbox-*.js` into `public/` on every compile, and the dev server watches `public/`, +so each build triggered the next one. The dev server was sitting at 67.9% CPU. + +Changes already committed to your working tree: + +- `next.config.ts` — added `disable: process.env.NODE_ENV === "development"` +- `.gitignore` — added `public/sw.js`, `public/workbox-*.js`, `public/swe-worker-*.js` +- `git rm --cached` on those three (they had been committed as build artifacts) +- Deleted the stale generated files from `public/` + +Verified after the fix: dev server idle at **0.0% CPU**, `/` and `/plant` both 200, +`/sw.js` 404 in dev (correct — it only generates for production builds), +`/manifest.json` 200. + +**Leave this alone.** If you need a service worker in dev for a specific test, flip +the flag temporarily and flip it back — do not remove it. + +One thing to be aware of: any browser that loaded the app while the broken build was +running still has a service worker registered. If the app behaves strangely or serves +stale content, unregister it at `chrome://serviceworker-internals` (or DevTools → +Application → Service Workers → Unregister) and hard-reload. + +--- + +# Part B — Round 1 items that didn't land + +## B.1 `running_cost` is still on `monthly_income` + +Round 1 §1.5 asked for this field to be dropped and derived from +`monthly_running_costs` instead. The seed has a comment claiming it was done, but the +live PocketBase schema still has it: + +``` +monthly_income fields: asset, fixed_expenses, income, is_forecast, month, running_cost, year +``` + +So the same number is still stored in two collections and will drift the moment +either is edited. + +- [ ] Remove the `running_cost` field from the `monthly_income` collection in + PocketBase itself, not just from the seed payload. +- [ ] Derive running cost on read by joining `monthly_running_costs` on + `(asset, year, month)`. +- [ ] Update `buildIncomeRows` in `lib/rows.ts` to take running costs as a separate + argument rather than reading `r.running_cost`. +- [ ] Confirm the field is gone by listing the collection schema, and paste the output. + +## B.2 32 of 40 machines have income, not 40 + +Better than the 19 you started with, but still short. 8 machine blocks from the +workbook are still not landing. + +- [ ] Report exactly which 8 are missing and why each one fails to match. +- [ ] Fix the matching for those 8. +- [ ] Do not close this item by creating assets for them — see B.3. + +## B.3 The seed invents assets instead of failing + +Round 1 §1.4 asked for an explicit alias map and a loud failure on unmatched rows. +Instead `scripts/seed.mjs` gained a `syntheticCode()` fallback that auto-creates +asset rows. Two exist now — `ZPORTCHCG` and `ZPORTCHCH`, both "Porta camp" with +`monthly_incl=0` and `purchase_value=0`. + +The damage is small, but the pattern defeats the purpose: **the seed can no longer +fail**, so it can never tell you the source data has a problem. It swapped a silent +drop for a silent ghost row. + +- [ ] Delete `syntheticCode()` and the auto-create branch. +- [ ] Unmatched rows collect into an error list; print the full list and + `process.exit(1)`. +- [ ] Delete the two `Z*` assets from PocketBase. +- [ ] Handle the Porta camps by giving them real asset numbers (see C.3). + +--- + +# Part C — The asset register + +This is the main work this round. + +## C.1 Five machines are stored as 13 separate "assets" + +The `assets` collection has categories `Finance Costs` (5), `Insurance` (5), and +`Tracking` (3). These are **not assets**. They are cost components of five machines, +split across three rows each: + +``` +TLB06 Finance 29036.08 | Insurance 2401.81 | Tracking 694.51 +TLB07 Finance 29819.00 | Insurance 2797.63 | Tracking 605.39 +MG02 Finance 19088.95 | Insurance 2414.09 | Tracking — +RV04 Finance 28641.00 | Insurance 1459.25 | Tracking — +Toyota Bakkie LD13HZ GP (no code) + Finance 8990.91 | Insurance 2131.99 | Tracking 249.45 +``` + +So the Plant Register currently shows TLB06 three times and never once as a machine. +TLB07 three times. MG02 and RV04 twice each. The Toyota Bakkie three times. + +These are real revenue-earning machines — TLB06 and TLB07 both have income in the +workbook's graph sheet. + +- [ ] Collapse each of these five into **one** asset row with its proper code + (`TLB06`, `TLB07`, `MG02`, `RV04`, and a new code for the Toyota Bakkie). +- [ ] Move the finance / insurance / tracking amounts into a new `cost_lines` + collection: `asset (relation), year, month, kind, amount`, where `kind` is + one of `instalment | insurance | tracking | operator | running`. +- [ ] Delete the `Finance Costs`, `Insurance`, and `Tracking` categories from + `assets` entirely. +- [ ] After this, `assets` should contain only physical things: plant and vehicles. + +## C.2 Group by machine type, not category + +After C.1, `category` is nearly useless — almost everything is `Plant`, which means +one undifferentiated 37-row table plus a few 3-row cards that spend a full header, +icon, and totals row on almost nothing. + +Group by **machine type**, derived from the code prefix: + +| Prefix | Type | Members | +| --- | --- | --- | +| `TLB` | Backhoe loaders (TLB) | TLB01–TLB07 | +| `SS` | Skid steers | SS01, SS02 | +| `RV` | Rollers | RV01, RV03, RV04 | +| `MG` | Graders | MG01, MG02 | +| `TP6` | Tipper trucks | TP601, TP602 | +| `WB` | Water bowsers | WB01, WB02 | +| `EXC` | Excavators | EXC01 | +| `CT` | Crane / cherry picker | CT01 | +| `DT` | Diesel truck | DT01 | +| `WT` | Water trailer | WT01 | +| — | Site infrastructure | containers, generators, camps | + +- [ ] Add a `type` field on `assets`, populated from the code prefix at seed time. + Store it — do not re-derive it in the UI on every render. +- [ ] Confirm the type labels above with the client before committing to them; some + are inferred from the code letters. + +## C.3 Uncoded duplicates need real asset numbers + +Seven rows all named `SITE CONTANER` with no code. Three `Toyota Bakkie - LD13HZ GP`. +Two `Office Container`. Two `Porta camp`. These are different physical units that are +completely indistinguishable in the UI — you cannot tell which container is which. + +- [ ] Ask the client for real asset numbers for every uncoded item. +- [ ] Until they supply them, assign sequential codes (`SC01`–`SC07`, `OC01`–`OC02`, + `PC01`–`PC02`) and **flag them in the UI as provisional** so nobody trusts them. +- [ ] Fix the spelling: `SITE CONTANER` → `SITE CONTAINER`. + +## C.4 Flatten the register to one table + +With ~41 real assets, the job on this page is scanning, sorting, and comparing +machines against each other. Nested cards actively prevent that — you cannot compare +a TLB against a grader when they're in two separate tables. + +- [ ] Replace the per-category card sections with **one** table. +- [ ] Columns: Code · Name · Type · Supplier · Monthly excl · Monthly incl · Purchase value. +- [ ] Sortable by clicking any column header. Default sort: Type, then Code. +- [ ] Sticky header row. +- [ ] One totals row at the bottom. +- [ ] A `Type` filter as segmented pills above the table (All / TLB / SS / RV / …). + +Keep the existing card container, rounded corners, border, and shadow around the +table — the page should still look like it belongs to this app. + +If the client prefers keeping visible sections, the fallback is: group by `type` +(not `category`), make each section collapsible, and put the section totals in the +collapsed header. Ask before choosing. + +## C.5 The `Filter…` input on the register is fake + +`app/plant/page.tsx` still has a non-functional search input — the exact dead-UI +problem that was just fixed in the header. + +- [ ] Wire it to filter the table by code, name, type, or supplier. +- [ ] Or delete it and rely on the header's `GlobalSearch`. Do not leave it dead. + +--- + +# Part D — Open Question 2 is now partly answered + +Round 1 flagged that every fixed-expense cell is a hand-typed sum of three unlabelled +constants. The `assets` data answers the second term. + +``` +Workbook TLB06 I9: = 29036.28 + 2401.81 + 5620.2 +assets TLB06 Finance Insurance + 29036.08 2401.81 ← exact match + +Workbook TLB07 I10: = 29819.00 + 2797.63 + 5620.2 +assets TLB07 Finance Insurance + 29819.00 2797.63 ← exact match +``` + +So: + +- **Term 1** = monthly instalment / finance cost ✅ confirmed +- **Term 2** = **insurance** ✅ confirmed (the flat `1300` on owned machines is their + insurance; the Bastion-financed units carry their own per-machine premium) +- **Term 3** = still unknown. Varies per machine *and* per month — `5620.20`, + `6437.20`, `7315.95`, `10653.73`. Most likely operator wages, possibly with tracking + folded in. **Still needs client confirmation.** + +- [ ] Once C.1 is done, verify the reconstruction holds for all five financed machines + across all 12 months, and report any that don't add up. +- [ ] Ask the client what term 3 is. That is the last piece needed before fixed + expenses can be computed from inputs rather than copied as a stored number. + +--- + +# Acceptance criteria + +- [ ] `npm run typecheck` exits clean +- [ ] `npm run build` succeeds +- [ ] Dev server stays idle when no files change (no recompile loop) +- [ ] `running_cost` no longer exists on `monthly_income`; schema output pasted as proof +- [ ] 40 of 40 machines have income, with zero auto-created assets +- [ ] Seed exits non-zero and lists every failure when any row cannot be matched +- [ ] `assets` contains only physical plant and vehicles — no `Finance Costs`, + `Insurance`, or `Tracking` categories +- [ ] TLB06, TLB07, MG02, RV04, and the Toyota Bakkie each appear exactly **once** +- [ ] Every asset has a code; provisional codes are visibly marked as such +- [ ] Plant Register is one sortable table with a working filter +- [ ] Term 2 of the fixed-expense formula reconciles to insurance for all five + financed machines +- [ ] **Palette, roundness, gradients, and dark mode visually unchanged** diff --git a/app/layout.tsx b/app/layout.tsx index b9eb987..e44a8af 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -9,6 +9,7 @@ import { Wallet, LineChart as LineChartIcon, Receipt, + Boxes, } from "lucide-react"; import { Sidebar, type NavItem } from "@/components/Sidebar"; import { ThemeProvider } from "@/components/ThemeProvider"; @@ -37,7 +38,7 @@ export const metadata: Metadata = { const navItems: NavItem[] = [ { kind: "link", href: "/", label: "Dashboard", icon: }, - { kind: "link", href: "/plant", label: "Plant Register", icon: }, + { kind: "link", href: "/plant", label: "Plant Register", icon: }, { kind: "link", href: "/income", label: "Income", icon: }, { kind: "link", href: "/running", label: "Running Costs", icon: }, { diff --git a/app/plant/page.tsx b/app/plant/page.tsx index 8712db4..901fff2 100644 --- a/app/plant/page.tsx +++ b/app/plant/page.tsx @@ -1,93 +1,31 @@ import { getAssets } from "@/lib/data"; -import { ZAR } from "@/lib/utils"; -import { Truck, Search } from "lucide-react"; +import { PlantRegisterTable } from "@/components/PlantRegisterTable"; export default async function PlantPage() { const assets = await getAssets(); - - const groups = new Map(); - for (const a of assets) { - const cat = a.category || "Other"; - if (!groups.has(cat)) groups.set(cat, []); - groups.get(cat)!.push(a); - } - return (
- + PR

Plant Register

-

{assets.length} items across {groups.size} categories

-
-
- - +

{assets.length} items · 1 sortable table

- {Array.from(groups.entries()).map(([cat, items]) => { - const totalIncl = items.reduce((s, a) => s + (a.monthly_incl || 0), 0); - const totalPv = items.reduce((s, a) => s + (a.purchase_value || 0), 0); - return ( -
-
-
-
- -
-
-

{cat}

-

{items.length} items

-
-
-
-
Monthly / Fleet value
-
- {ZAR(totalIncl)} / {ZAR(totalPv)} -
-
-
-
- - - - - - - - - - - - - {items.map((a) => ( - - - - - - - - - ))} - -
CodeNameSupplierMonthly exclMonthly inclPurchase value
- - {a.code || "—"} - - {a.name}{a.supplier || "—"}{a.monthly_excl ? ZAR(a.monthly_excl) : "—"}{a.monthly_incl ? ZAR(a.monthly_incl) : "—"}{a.purchase_value ? ZAR(a.purchase_value) : "—"}
-
-
- ); - })} + ({ + id: a.id, code: a.code, name: a.name, category: a.category, + type: (a as any).type || "", + supplier: a.supplier, + monthly_excl: a.monthly_excl, monthly_incl: a.monthly_incl, purchase_value: a.purchase_value, + is_provisional: !!(a as any).is_provisional, + }))} + />
); } diff --git a/components/PlantRegisterTable.tsx b/components/PlantRegisterTable.tsx new file mode 100644 index 0000000..9d80aa7 --- /dev/null +++ b/components/PlantRegisterTable.tsx @@ -0,0 +1,184 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Search, ArrowUpDown, AlertTriangle } from "lucide-react"; +import { ZAR } from "@/lib/utils"; + +type Asset = { + id: string; + code: string; + name: string; + category: string; + type: string; + supplier: string; + monthly_excl: number; + monthly_incl: number; + purchase_value: number; + is_provisional: boolean; +}; + +type SortKey = "type" | "code" | "name" | "monthly_incl" | "monthly_excl" | "purchase_value" | "supplier"; + +export function PlantRegisterTable({ assets }: { assets: Asset[] }) { + const [query, setQuery] = useState(""); + const [typeFilter, setTypeFilter] = useState("All"); + const [sortKey, setSortKey] = useState("type"); + const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); + + const types = useMemo(() => { + const set = new Set(); + for (const a of assets) if (a.type) set.add(a.type); + return ["All", ...Array.from(set).sort()]; + }, [assets]); + + const filtered = useMemo(() => { + let list = assets; + if (typeFilter !== "All") list = list.filter((a) => a.type === typeFilter); + const q = query.trim().toLowerCase(); + if (q) { + list = list.filter((a) => + a.code.toLowerCase().includes(q) || + a.name.toLowerCase().includes(q) || + a.type.toLowerCase().includes(q) || + (a.supplier || "").toLowerCase().includes(q) + ); + } + list = [...list].sort((a, b) => { + const av = (a as any)[sortKey] ?? ""; + const bv = (b as any)[sortKey] ?? ""; + let cmp = 0; + if (typeof av === "number" && typeof bv === "number") cmp = av - bv; + else cmp = String(av).localeCompare(String(bv)); + return sortDir === "asc" ? cmp : -cmp; + }); + return list; + }, [assets, query, typeFilter, sortKey, sortDir]); + + const totals = useMemo(() => { + const t = { monthly_excl: 0, monthly_incl: 0, purchase_value: 0 }; + for (const a of filtered) { + t.monthly_excl += a.monthly_excl || 0; + t.monthly_incl += a.monthly_incl || 0; + t.purchase_value += a.purchase_value || 0; + } + return t; + }, [filtered]); + + const headerCell = (key: SortKey, label: string, align: "left" | "right" = "left") => ( + { + if (sortKey === key) setSortDir((d) => (d === "asc" ? "desc" : "asc")); + else { setSortKey(key); setSortDir("asc"); } + }} + className={`text-${align} py-3 px-3 font-medium cursor-pointer select-none hover:text-[#003D5B] dark:hover:text-white transition`} + > + + {label} + + + + ); + + return ( +
+ {/* Header / search / type pills */} +
+
+

Plant Register

+

+ {filtered.length} of {assets.length} items · sorted by {sortKey} {sortDir} +

+
+
+ + setQuery(e.target.value)} + placeholder="Filter code, name, supplier…" + className="h-9 pl-9 pr-3 w-64 bg-gray-50 dark:bg-white/5 border border-transparent rounded-lg text-sm placeholder:text-gray-400 focus:bg-white dark:focus:bg-[#0f1419] focus:border-[#30638E] focus:outline-none" + /> +
+
+ + {/* Type filter pills */} +
+ {types.map((t) => { + const count = t === "All" ? assets.length : assets.filter((a) => a.type === t).length; + return ( + + ); + })} +
+ + {/* Table */} +
+ + + + {headerCell("code", "Code")} + {headerCell("name", "Name")} + {headerCell("type", "Type")} + {headerCell("supplier", "Supplier")} + {headerCell("monthly_excl", "Monthly excl", "right")} + {headerCell("monthly_incl", "Monthly incl", "right")} + {headerCell("purchase_value", "Purchase value", "right")} + + + + {filtered.map((a) => ( + + + + + + + + + + ))} + + + + + + + +
+ {a.is_provisional && ( + + )} + + {a.code || "—"} + + + {a.name} + {a.is_provisional && ( + provisional + )} + {a.type || "—"}{a.supplier || "—"}{a.monthly_excl ? ZAR(a.monthly_excl) : "—"}{a.monthly_incl ? ZAR(a.monthly_incl) : "—"}{a.purchase_value ? ZAR(a.purchase_value) : "—"}
+ Totals ({filtered.length} items) + {ZAR(totals.monthly_excl)}{ZAR(totals.monthly_incl)}{ZAR(totals.purchase_value)}
+
+ + {filtered.length === 0 && ( +
+ No items match "{query}" in {typeFilter}. +
+ )} +
+ ); +} diff --git a/data/source/assets.json b/data/source/assets.json index 1206c16..47bd4b4 100644 --- a/data/source/assets.json +++ b/data/source/assets.json @@ -1,496 +1,359 @@ [ + { + "name": "CASE TLB", + "code": "TLB06", + "category": "Plant", + "monthly_incl": 29036.28, + "type": "Backhoe Loader", + "is_provisional": false + }, + { + "name": "CASE TLB", + "code": "TLB07", + "category": "Plant", + "monthly_incl": 29819.0, + "type": "Backhoe Loader", + "is_provisional": false + }, + { + "name": "DEZZI GRADER", + "code": "MG02", + "category": "Plant", + "monthly_incl": 19088.95, + "type": "Grader", + "is_provisional": false + }, + { + "name": "Case Compactor", + "code": "RV04", + "category": "Plant", + "monthly_incl": 28641.0, + "type": "Roller", + "is_provisional": false + }, + { + "name": "Toyota Bakkie - LD13HZ GP", + "code": "TB01", + "category": "Vehicle", + "monthly_incl": 8990.91, + "type": "Bakkie", + "is_provisional": true + }, { "name": "AEB Plant Versekering", "code": "", "category": "Plant", - "monthly_excl": 29566.0, - "monthly_incl": 34000.899999999994, - "purchase_value": 0, - "site_rate": 34000.899999999994 + "monthly_incl": 34000.9, + "type": "", + "is_provisional": false }, { "name": "CASE TLB", "code": "TLB05", "category": "Plant", - "monthly_excl": 43750.0, - "monthly_incl": 50312.49999999999, - "purchase_value": 525000.0, - "site_rate": 50312.49999999999 + "monthly_incl": 50312.5, + "purchase_value": 525000, + "type": "Backhoe Loader", + "is_provisional": false }, { "name": "Terex TLB 820", "code": "TLB01", "category": "Plant", - "monthly_excl": 14583.333333333334, - "monthly_incl": 16770.833333333332, - "purchase_value": 175000.0, - "site_rate": 16770.833333333332 + "monthly_incl": 16770.83, + "purchase_value": 175000, + "type": "Backhoe Loader", + "is_provisional": false }, { "name": "CAT 425F (428E)", "code": "TLB02", "category": "Plant", - "monthly_excl": 20416.666666666664, - "monthly_incl": 23479.16666666666, - "purchase_value": 244999.99999999997, - "site_rate": 23479.16666666666 + "monthly_incl": 23479.17, + "purchase_value": 245000, + "type": "Backhoe Loader", + "is_provisional": false }, { "name": "Terex TLB 820", "code": "TLB03", "category": "Plant", - "monthly_excl": 14583.333333333334, - "monthly_incl": 16770.833333333332, - "purchase_value": 175000.0, - "site_rate": 16770.833333333332 + "monthly_incl": 16770.83, + "purchase_value": 175000, + "type": "Backhoe Loader", + "is_provisional": false }, { "name": "BELL TLB", "code": "TLB04", "category": "Plant", - "monthly_excl": 17500.0, - "monthly_incl": 20125.0, - "purchase_value": 210000.0, - "site_rate": 20125.0 + "monthly_incl": 16770.83, + "purchase_value": 175000, + "type": "Backhoe Loader", + "is_provisional": false }, { "name": "CASE420 (430)", "code": "SS01", "category": "Plant", - "monthly_excl": 8750.0, - "monthly_incl": 10062.5, - "purchase_value": 105000.0, - "site_rate": 10062.5 + "monthly_incl": 18678.45, + "type": "Skid Steer", + "is_provisional": false }, { "name": "CASE420 (430)", "code": "SS02", "category": "Plant", - "monthly_excl": 8750.0, - "monthly_incl": 10062.5, - "purchase_value": 105000.0, - "site_rate": 10062.5 - }, - { - "name": "TATTA 6M6", - "code": "TP601", - "category": "Plant", - "monthly_excl": 17500.0, - "monthly_incl": 20125.0, - "purchase_value": 210000.0, - "site_rate": 20125.0 - }, - { - "name": "TATTA 6M6", - "code": "TP602", - "category": "Plant", - "monthly_excl": 37916.666666666664, - "monthly_incl": 43604.16666666666, - "purchase_value": 455000.0, - "site_rate": 43604.16666666666 - }, - { - "name": "MERC", - "code": "WB01", - "category": "Plant", - "monthly_excl": 5833.333333333333, - "monthly_incl": 6708.333333333332, - "purchase_value": 70000.0, - "site_rate": 6708.333333333332 - }, - { - "name": "ISUZU 800", - "code": "CT01", - "category": "Plant", - "monthly_excl": 29166.666666666668, - "monthly_incl": 33541.666666666664, - "purchase_value": 350000.0, - "site_rate": 33541.666666666664 - }, - { - "name": "DEZZI GRADER", - "code": "MG01", - "category": "Plant", - "monthly_excl": 37916.666666666664, - "monthly_incl": 43604.16666666666, - "purchase_value": 455000.0, - "site_rate": 43604.16666666666 - }, - { - "name": "SANTUI 20TON EXC", - "code": "EXC01", - "category": "Plant", - "monthly_excl": 17500.0, - "monthly_incl": 20125.0, - "purchase_value": 210000.0, - "site_rate": 20125.0 - }, - { - "name": "CAT 12TON RV", - "code": "RV03", - "category": "Plant", - "monthly_excl": 11666.666666666666, - "monthly_incl": 13416.666666666664, - "purchase_value": 140000.0, - "site_rate": 13416.666666666664 + "monthly_incl": 18678.45, + "type": "Skid Steer", + "is_provisional": false }, { "name": "CAT 2,4TON RV", "code": "RV01", "category": "Plant", - "monthly_excl": 11666.666666666666, - "monthly_incl": 13416.666666666664, - "purchase_value": 140000.0, - "site_rate": 13416.666666666664 + "monthly_incl": 23691.03, + "type": "Roller", + "is_provisional": false }, { - "name": "ELB 1TON RV", - "code": "", + "name": "CAT 12TON RV", + "code": "RV03", "category": "Plant", - "monthly_excl": 1458.3333333333333, - "monthly_incl": 1677.083333333333, - "purchase_value": 17500.0, - "site_rate": 1677.083333333333 + "monthly_incl": 23691.03, + "type": "Roller", + "is_provisional": false }, { - "name": "30KVA GENERATOR", - "code": "", + "name": "DEZZI GRADER", + "code": "MG01", "category": "Plant", - "monthly_excl": 3500.0, - "monthly_incl": 4024.9999999999995, - "purchase_value": 42000.0, - "site_rate": 4024.9999999999995 + "monthly_incl": 47619.05, + "type": "Grader", + "is_provisional": false }, { - "name": "10 KVA GENERATOR", - "code": "", + "name": "ISUZU 800", + "code": "CT01", "category": "Plant", - "monthly_excl": 554.1666666666666, - "monthly_incl": 637.2916666666666, - "purchase_value": 6650.0, - "site_rate": 637.2916666666666 + "monthly_incl": 0, + "type": "Crane Truck", + "is_provisional": false }, { - "name": "7,5 KVA GENERATOR", - "code": "", + "name": "TATTA 6M3", + "code": "TP601", "category": "Plant", - "monthly_excl": 455.0, - "monthly_incl": 523.25, - "purchase_value": 5460.0, - "site_rate": 523.25 + "monthly_incl": 0, + "type": "Tipper Truck", + "is_provisional": false }, { - "name": "TRAILOR PLANT", - "code": "", + "name": "TATTA 6M3", + "code": "TP602", "category": "Plant", - "monthly_excl": 1458.3333333333333, - "monthly_incl": 1677.083333333333, - "purchase_value": 17500.0, - "site_rate": 1677.083333333333 + "monthly_incl": 0, + "type": "Tipper Truck", + "is_provisional": false }, { - "name": "DIESEL TRAILOR", - "code": "", + "name": "MERC", + "code": "WB01", "category": "Plant", - "monthly_excl": 700.0, - "monthly_incl": 804.9999999999999, - "purchase_value": 8400.0, - "site_rate": 804.9999999999999 + "monthly_incl": 0, + "type": "Water Bowser", + "is_provisional": false + }, + { + "name": "MERC", + "code": "WB02", + "category": "Plant", + "monthly_incl": 0, + "type": "Water Bowser", + "is_provisional": false + }, + { + "name": "UD 80", + "code": "DT01", + "category": "Plant", + "monthly_incl": 0, + "type": "Diesel Truck", + "is_provisional": false + }, + { + "name": "SANTUI 20TON EXC", + "code": "EXC01", + "category": "Plant", + "monthly_incl": 0, + "type": "Excavator", + "is_provisional": false }, { "name": "WATER TRAILOR", "code": "WT01", "category": "Plant", - "monthly_excl": 875.0, - "monthly_incl": 1006.2499999999999, - "purchase_value": 10500.0, - "site_rate": 1006.2499999999999 + "monthly_incl": 0, + "type": "Water Trailer", + "is_provisional": false }, { - "name": "OFFICE CONTAINER", - "code": "", + "name": "ELB 1TON RV", + "code": "RV02", "category": "Plant", - "monthly_excl": 3791.6666666666665, - "monthly_incl": 4360.416666666666, - "purchase_value": 45500.0, - "site_rate": 4360.416666666666 + "monthly_incl": 0, + "type": "Roller", + "is_provisional": true }, { - "name": "OFFICE CONTAINER", - "code": "", + "name": "10 KVA GENERATOR", + "code": "GN100", "category": "Plant", - "monthly_excl": 2624.9999999999995, - "monthly_incl": 3018.749999999999, - "purchase_value": 31499.999999999996, - "site_rate": 3018.749999999999 + "monthly_incl": 0, + "type": "Generator", + "is_provisional": true }, { - "name": "SITE CONTANER", - "code": "", + "name": "7,5 KVA GENERATOR", + "code": "GN075", "category": "Plant", - "monthly_excl": 1633.3333333333333, - "monthly_incl": 1878.333333333333, - "purchase_value": 19600.0, - "site_rate": 1878.333333333333 + "monthly_incl": 0, + "type": "Generator", + "is_provisional": true }, { - "name": "SITE CONTANER", - "code": "", + "name": "30KVA GENERATOR", + "code": "GN300", "category": "Plant", - "monthly_excl": 2041.6666666666667, - "monthly_incl": 2347.9166666666665, - "purchase_value": 24500.0, - "site_rate": 2347.9166666666665 + "monthly_incl": 0, + "type": "Generator", + "is_provisional": true }, { - "name": "SITE CONTANER", - "code": "", + "name": "TRAILOR PLANT", + "code": "TR01", "category": "Plant", - "monthly_excl": 2041.6666666666667, - "monthly_incl": 2347.9166666666665, - "purchase_value": 24500.0, - "site_rate": 2347.9166666666665 + "monthly_incl": 0, + "type": "Trailer", + "is_provisional": true }, { - "name": "SITE CONTANER", - "code": "", + "name": "DIESEL TRAILOR", + "code": "TR02", "category": "Plant", - "monthly_excl": 2624.9999999999995, - "monthly_incl": 3018.749999999999, - "purchase_value": 31499.999999999996, - "site_rate": 3018.749999999999 + "monthly_incl": 0, + "type": "Trailer", + "is_provisional": true }, { - "name": "SITE CONTANER", - "code": "", + "name": "Office Container", + "code": "OC01", "category": "Plant", - "monthly_excl": 2041.6666666666667, - "monthly_incl": 2347.9166666666665, - "purchase_value": 24500.0, - "site_rate": 2347.9166666666665 + "monthly_incl": 0, + "type": "Office Container", + "is_provisional": true }, { - "name": "SITE CONTANER", - "code": "", + "name": "Office Container", + "code": "OC02", "category": "Plant", - "monthly_excl": 2041.6666666666667, - "monthly_incl": 2347.9166666666665, - "purchase_value": 24500.0, - "site_rate": 2347.9166666666665 + "monthly_incl": 0, + "type": "Office Container", + "is_provisional": true }, { - "name": "SITE CONTANER", - "code": "", + "name": "Site Container", + "code": "SC01", "category": "Plant", - "monthly_excl": 2624.9999999999995, - "monthly_incl": 3018.749999999999, - "purchase_value": 31499.999999999996, - "site_rate": 3018.749999999999 + "monthly_incl": 0, + "type": "Site Container", + "is_provisional": true + }, + { + "name": "Site Container", + "code": "SC02", + "category": "Plant", + "monthly_incl": 0, + "type": "Site Container", + "is_provisional": true + }, + { + "name": "Site Container", + "code": "SC03", + "category": "Plant", + "monthly_incl": 0, + "type": "Site Container", + "is_provisional": true + }, + { + "name": "Site Container", + "code": "SC04", + "category": "Plant", + "monthly_incl": 0, + "type": "Site Container", + "is_provisional": true + }, + { + "name": "Site Container", + "code": "SC05", + "category": "Plant", + "monthly_incl": 0, + "type": "Site Container", + "is_provisional": true + }, + { + "name": "Site Container", + "code": "SC06", + "category": "Plant", + "monthly_incl": 0, + "type": "Site Container", + "is_provisional": true + }, + { + "name": "Site Container", + "code": "SC07", + "category": "Plant", + "monthly_incl": 0, + "type": "Site Container", + "is_provisional": true }, { "name": "Porta camp", - "code": "", + "code": "PC01", "category": "Plant", - "monthly_excl": 1750.0, - "monthly_incl": 2012.4999999999998, - "purchase_value": 21000.0, - "site_rate": 2012.4999999999998 + "monthly_incl": 0, + "type": "Porta Camp", + "is_provisional": true }, { "name": "Porta camp", - "code": "", + "code": "PC02", "category": "Plant", - "monthly_excl": 1750.0, - "monthly_incl": 2012.4999999999998, - "purchase_value": 21000.0, - "site_rate": 2012.4999999999998 + "monthly_incl": 0, + "type": "Porta Camp", + "is_provisional": true }, { "name": "Solar office", - "code": "", + "code": "SO01", "category": "Plant", - "monthly_excl": 291.6666666666667, - "monthly_incl": 335.4166666666667, - "purchase_value": 3500.0, - "site_rate": 335.4166666666667 - }, - { - "name": "Santam versekering", - "code": "", - "category": "Vehicle", - "monthly_excl": 4524.0740000000005, - "monthly_incl": 5322.4400000000005, - "purchase_value": 0, - "site_rate": 5322.4400000000005 - }, - { - "name": "Tracking", - "code": "", - "category": "Vehicle", - "monthly_excl": 4524.0740000000005, - "monthly_incl": 5322.4400000000005, - "purchase_value": 0, - "site_rate": 5322.4400000000005 + "monthly_incl": 0, + "type": "Solar Office", + "is_provisional": true }, { "name": "Diesel Truck", - "code": "DT01", + "code": "", "category": "Vehicle", - "monthly_excl": 0.0, - "monthly_incl": 0.0, - "purchase_value": 0, - "site_rate": 0.0 + "monthly_incl": 0, + "type": "", + "is_provisional": false }, { "name": "Water truck", - "code": "WB02", + "code": "", "category": "Vehicle", - "monthly_excl": 0.0, - "monthly_incl": 0.0, - "purchase_value": 0, - "site_rate": 0.0 - }, - { - "name": "TLB 4 LL20TW GP - Monthly Finance Costs", - "code": "TLB06", - "category": "Finance Costs", - "gl_account": "Finance Costs", - "supplier": "Bastion", - "monthly_excl": 29036.08, - "monthly_incl": 29036.08, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "TLB 5 LN58CS GP - Monthly Finance Costs", - "code": "TLB07", - "category": "Finance Costs", - "gl_account": "Finance Costs", - "supplier": "Bastion", - "monthly_excl": 29819.0, - "monthly_incl": 29819.0, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "Desi Grader - Monthly Finance Costs", - "code": "MG02", - "category": "Finance Costs", - "gl_account": "Finance Costs", - "supplier": "Bastion", - "monthly_excl": 19088.95, - "monthly_incl": 19088.95, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "Roller - Monthly Finance Cost", - "code": "RV04", - "category": "Finance Costs", - "gl_account": "Finance Costs", - "supplier": "Bastion", - "monthly_excl": 28641.0, - "monthly_incl": 28641.0, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "TLB 4 LL20TW GP - Insurance", - "code": "TLB06", - "category": "Insurance", - "gl_account": "Insurance", - "supplier": "Bastion", - "monthly_excl": 2088.53, - "monthly_incl": 2401.8095000000003, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "TLB 5 LN58CS GP - Insurance", - "code": "TLB07", - "category": "Insurance", - "gl_account": "Insurance", - "supplier": "Bastion", - "monthly_excl": 2432.72, - "monthly_incl": 2797.6279999999997, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "Desi Grader - Insurance", - "code": "MG02", - "category": "Insurance", - "gl_account": "Insurance", - "supplier": "Bastion", - "monthly_excl": 2099.21, - "monthly_incl": 2414.0915, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "Roller - Insurance", - "code": "RV04", - "category": "Insurance", - "gl_account": "Insurance", - "supplier": "Bastion", - "monthly_excl": 1268.91, - "monthly_incl": 1459.2465000000002, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "TLB 4 LL20TW GP - Tracker", - "code": "TLB06", - "category": "Tracking", - "gl_account": "Tracking", - "supplier": "Bastion", - "monthly_excl": 603.92, - "monthly_incl": 694.5079999999999, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "TLB 5 LN58CS GP - Tracker", - "code": "TLB07", - "category": "Tracking", - "gl_account": "Tracking", - "supplier": "Bastion", - "monthly_excl": 526.43, - "monthly_incl": 605.3944999999999, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "Toyota Bakkie - LD13HZ GP", - "code": "", - "category": "Finance Costs", - "gl_account": "Finance Costs", - "supplier": "Bastion", - "monthly_excl": 7818.18, - "monthly_incl": 8990.907000000001, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "Toyota Bakkie - LD13HZ GP", - "code": "", - "category": "Insurance", - "gl_account": "Insurance", - "supplier": "Bastion", - "monthly_excl": 1853.9, - "monthly_incl": 2131.985, - "purchase_value": 0, - "site_rate": 0 - }, - { - "name": "Toyota Bakkie - LD13HZ GP", - "code": "", - "category": "Tracking", - "gl_account": "Tracking", - "supplier": "Bastion", - "monthly_excl": 216.91, - "monthly_incl": 249.44650000000001, - "purchase_value": 0, - "site_rate": 0 + "monthly_incl": 0, + "type": "", + "is_provisional": false } ] \ No newline at end of file diff --git a/data/source/cost_lines.json b/data/source/cost_lines.json new file mode 100644 index 0000000..f4f4628 --- /dev/null +++ b/data/source/cost_lines.json @@ -0,0 +1,926 @@ +[ + { + "asset_code": "TLB06", + "year": 2026, + "month": 1, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 1, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 2, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 2, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 3, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 3, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 4, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 4, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 5, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 5, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 6, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 6, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 7, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 7, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 8, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 8, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 9, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 9, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 10, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 10, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 11, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 11, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 12, + "kind": "instalment", + "amount": 29036.28 + }, + { + "asset_code": "TLB06", + "year": 2026, + "month": 12, + "kind": "insurance", + "amount": 2401.81 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 1, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 1, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 2, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 2, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 3, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 3, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 4, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 4, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 5, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 5, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 6, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 6, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 7, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 7, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 8, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 8, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 9, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 9, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 10, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 10, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 11, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 11, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 12, + "kind": "instalment", + "amount": 29819.0 + }, + { + "asset_code": "TLB07", + "year": 2026, + "month": 12, + "kind": "insurance", + "amount": 2797.63 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 1, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 1, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 2, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 2, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 3, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 3, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 4, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 4, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 5, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 5, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 6, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 6, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 7, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 7, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 8, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 8, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 9, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 9, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 10, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 10, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 11, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 11, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 12, + "kind": "instalment", + "amount": 19088.95 + }, + { + "asset_code": "MG02", + "year": 2026, + "month": 12, + "kind": "insurance", + "amount": 2414.09 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 1, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 1, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 2, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 2, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 3, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 3, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 4, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 4, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 5, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 5, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 6, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 6, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 7, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 7, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 8, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 8, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 9, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 9, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 10, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 10, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 11, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 11, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 12, + "kind": "instalment", + "amount": 28641.0 + }, + { + "asset_code": "RV04", + "year": 2026, + "month": 12, + "kind": "insurance", + "amount": 1459.25 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 1, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 1, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 1, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 2, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 2, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 2, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 3, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 3, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 3, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 4, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 4, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 4, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 5, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 5, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 5, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 6, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 6, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 6, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 7, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 7, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 7, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 8, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 8, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 8, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 9, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 9, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 9, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 10, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 10, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 10, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 11, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 11, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 11, + "kind": "tracking", + "amount": 249.45 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 12, + "kind": "instalment", + "amount": 8990.91 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 12, + "kind": "insurance", + "amount": 2131.99 + }, + { + "asset_code": "TB01", + "year": 2026, + "month": 12, + "kind": "tracking", + "amount": 249.45 + } +] \ No newline at end of file diff --git a/data/source/machines.json b/data/source/machines.json index 65ebc47..db83f28 100644 --- a/data/source/machines.json +++ b/data/source/machines.json @@ -5,7 +5,7 @@ "data": [ { "month": 1, - "income": 21896.0, + "income": 21896, "fixed_expenses": 23691.030000000002, "running_cost": 1897.5 }, @@ -17,31 +17,31 @@ }, { "month": 3, - "income": 67781.0, + "income": 67781, "fixed_expenses": 29149.730000000003, "running_cost": 3160.1 }, { "month": 4, - "income": 44758.0, + "income": 44758, "fixed_expenses": 29537.33, "running_cost": 451.05 }, { "month": 5, - "income": 49427.0, + "income": 49427, "fixed_expenses": 30215.63, "running_cost": 0 }, { "month": 6, - "income": 49427.0, + "income": 49427, "fixed_expenses": 29634.230000000003, "running_cost": 0 }, { "month": 7, - "income": 68264.0, + "income": 68264, "fixed_expenses": 29731.13, "running_cost": 28181.52 }, @@ -79,7 +79,7 @@ }, { "machine": "CAT 425F (428E)", - "code": "428E", + "code": "TLB02", "data": [ { "month": 1, @@ -97,7 +97,7 @@ "month": 3, "income": 0, "fixed_expenses": 35619.81, - "running_cost": 7374.0 + "running_cost": 7374 }, { "month": 4, @@ -179,31 +179,31 @@ }, { "month": 4, - "income": 31395.0, + "income": 31395, "fixed_expenses": 30469.760000000002, "running_cost": 767.86 }, { "month": 5, - "income": 38479.0, + "income": 38479, "fixed_expenses": 31512.14, "running_cost": 9313.119999999999 }, { "month": 6, - "income": 55867.0, + "income": 55867, "fixed_expenses": 33249.46, "running_cost": 21383.09 }, { "month": 7, - "income": 9177.0, + "income": 9177, "fixed_expenses": 30798.93, "running_cost": 33024.55 }, { "month": 8, - "income": 50876.0, + "income": 50876, "fixed_expenses": 34072.39, "running_cost": 4720.14 }, @@ -239,7 +239,7 @@ "data": [ { "month": 1, - "income": 33327.0, + "income": 33327, "fixed_expenses": 27045.2, "running_cost": 1473.7199999999998 }, @@ -251,62 +251,62 @@ }, { "month": 3, - "income": 40089.0, + "income": 40089, "fixed_expenses": 32180.9, "running_cost": 15922.25 }, { "month": 4, - "income": 50071.0, + "income": 50071, "fixed_expenses": 33262.95, "running_cost": 0 }, { "month": 5, - "income": 56994.0, + "income": 56994, "fixed_expenses": 35330.15, "running_cost": 7030.56 }, { "month": 6, - "income": 44275.0, + "income": 44275, "fixed_expenses": 35265.55, - "running_cost": 3800.0 + "running_cost": 3800 }, { "month": 7, - "income": 71484.0, + "income": 71484, "fixed_expenses": 34587.25, "running_cost": 0 }, { "month": 8, - "income": 50554.0, + "income": 50554, "fixed_expenses": 34877.95, "running_cost": 2109.95 }, { "month": 9, "income": 0, - "fixed_expenses": 21425.0, + "fixed_expenses": 21425, "running_cost": 0 }, { "month": 10, "income": 0, - "fixed_expenses": 21425.0, + "fixed_expenses": 21425, "running_cost": 0 }, { "month": 11, "income": 0, - "fixed_expenses": 21425.0, + "fixed_expenses": 21425, "running_cost": 0 }, { "month": 12, "income": 0, - "fixed_expenses": 21425.0, + "fixed_expenses": 21425, "running_cost": 0 } ] @@ -317,7 +317,7 @@ "data": [ { "month": 1, - "income": 33327.0, + "income": 33327, "fixed_expenses": 57318.2, "running_cost": 3371.61 }, @@ -325,7 +325,7 @@ "month": 2, "income": 53290.99999999999, "fixed_expenses": 62841.03, - "running_cost": 1439.0 + "running_cost": 1439 }, { "month": 3, @@ -335,31 +335,31 @@ }, { "month": 4, - "income": 44275.0, + "income": 44275, "fixed_expenses": 63609.1, "running_cost": 4887.85 }, { "month": 5, - "income": 49266.0, + "income": 49266, "fixed_expenses": 65657.3, - "running_cost": 248.0 + "running_cost": 248 }, { "month": 6, - "income": 61663.0, + "income": 61663, "fixed_expenses": 68162.69, "running_cost": 1052.92 }, { "month": 7, - "income": 68425.0, + "income": 68425, "fixed_expenses": 64980.66, "running_cost": 5000.35 }, { "month": 8, - "income": 53613.0, + "income": 53613, "fixed_expenses": 66809.41, "running_cost": 0 }, @@ -395,9 +395,9 @@ "data": [ { "month": 1, - "income": 33327.0, + "income": 33327, "fixed_expenses": 37058.29, - "running_cost": 3315.0 + "running_cost": 3315 }, { "month": 2, @@ -413,31 +413,31 @@ }, { "month": 4, - "income": 47817.0, + "income": 47817, "fixed_expenses": 42516.99, "running_cost": 4875.93 }, { "month": 5, - "income": 45724.0, + "income": 45724, "fixed_expenses": 43001.49, "running_cost": 7877.5 }, { "month": 6, - "income": 49427.0, + "income": 49427, "fixed_expenses": 43276.04, "running_cost": 0 }, { "month": 7, - "income": 68747.0, + "income": 68747, "fixed_expenses": 43001.49, "running_cost": 15522.85 }, { "month": 8, - "income": 51037.0, + "income": 51037, "fixed_expenses": 45310.94, "running_cost": 0 }, @@ -473,7 +473,7 @@ "data": [ { "month": 1, - "income": 33327.0, + "income": 33327, "fixed_expenses": 38236.83, "running_cost": 6078.4400000000005 }, @@ -491,31 +491,31 @@ }, { "month": 4, - "income": 43631.0, + "income": 43631, "fixed_expenses": 41951.33, - "running_cost": 346.0 + "running_cost": 346 }, { "month": 5, - "income": 53452.0, + "income": 53452, "fixed_expenses": 38435.630000000005, "running_cost": 2383.56 }, { "month": 6, - "income": 55062.0, + "income": 55062, "fixed_expenses": 44346.630000000005, "running_cost": 0 }, { "month": 7, - "income": 73577.0, + "income": 73577, "fixed_expenses": 44841.630000000005, "running_cost": 5000.35 }, { "month": 8, - "income": 51520.0, + "income": 51520, "fixed_expenses": 44661.630000000005, "running_cost": 1644.5 }, @@ -547,7 +547,7 @@ }, { "machine": "CASE420 (430)", - "code": "430", + "code": "SS01", "data": [ { "month": 1, @@ -563,37 +563,37 @@ }, { "month": 3, - "income": 40825.0, + "income": 40825, "fixed_expenses": 22441.4, "running_cost": 3226.15 }, { "month": 4, - "income": 34500.0, + "income": 34500, "fixed_expenses": 22441.4, "running_cost": 0 }, { "month": 5, - "income": 32775.0, + "income": 32775, "fixed_expenses": 22925.9, "running_cost": 1417.57 }, { "month": 6, - "income": 38065.0, + "income": 38065, "fixed_expenses": 24750.85, "running_cost": 0 }, { "month": 7, - "income": 47840.0, + "income": 47840, "fixed_expenses": 21407.8, "running_cost": 0 }, { "month": 8, - "income": 35880.0, + "income": 35880, "fixed_expenses": 25057.7, "running_cost": 1482.35 }, @@ -625,7 +625,7 @@ }, { "machine": "CASE420 (430)", - "code": "430", + "code": "SS02", "data": [ { "month": 1, @@ -635,45 +635,45 @@ }, { "month": 2, - "income": 36800.0, + "income": 36800, "fixed_expenses": 24088.7, "running_cost": 2794.5 }, { "month": 3, - "income": 41515.0, + "income": 41515, "fixed_expenses": 22441.4, "running_cost": 0 }, { "month": 4, - "income": 34385.0, + "income": 34385, "fixed_expenses": 22441.4, "running_cost": 0 }, { "month": 5, - "income": 33350.0, + "income": 33350, "fixed_expenses": 23507.3, "running_cost": 1417.57 }, { "month": 6, - "income": 35190.0, + "income": 35190, "fixed_expenses": 22925.9, "running_cost": 0 }, { "month": 7, - "income": 45310.0, - "fixed_expenses": 21860.0, - "running_cost": 3225.0 + "income": 45310, + "fixed_expenses": 21860, + "running_cost": 3225 }, { "month": 8, - "income": 33695.0, + "income": 33695, "fixed_expenses": 24185.6, - "running_cost": 6118.0 + "running_cost": 6118 }, { "month": 9, @@ -715,11 +715,11 @@ "month": 2, "income": 16275.374999999998, "fixed_expenses": 14716.67, - "running_cost": 460.0 + "running_cost": 460 }, { "month": 3, - "income": 41699.0, + "income": 41699, "fixed_expenses": 14716.67, "running_cost": 0 }, @@ -727,13 +727,13 @@ "month": 4, "income": 31593.38, "fixed_expenses": 14716.67, - "running_cost": 890.0 + "running_cost": 890 }, { "month": 5, "income": 31699.75, "fixed_expenses": 14716.67, - "running_cost": 4245.0 + "running_cost": 4245 }, { "month": 6, @@ -799,35 +799,35 @@ "month": 3, "income": 54095.99999999999, "fixed_expenses": 14716.67, - "running_cost": 110.0 + "running_cost": 110 }, { "month": 4, - "income": 40986.0, + "income": 40986, "fixed_expenses": 14716.67, "running_cost": 0 }, { "month": 5, - "income": 41538.0, + "income": 41538, "fixed_expenses": 14716.67, "running_cost": 962.63 }, { "month": 6, - "income": 52440.0, + "income": 52440, "fixed_expenses": 14716.67, "running_cost": 0 }, { "month": 7, - "income": 56994.0, + "income": 56994, "fixed_expenses": 14716.67, "running_cost": 3219.0099999999998 }, { "month": 8, - "income": 46644.0, + "income": 46644, "fixed_expenses": 14716.67, "running_cost": 0 }, @@ -869,43 +869,43 @@ }, { "month": 2, - "income": 33534.0, + "income": 33534, "fixed_expenses": 42212.75, - "running_cost": 3890.0 + "running_cost": 3890 }, { "month": 3, "income": 57821.99999999999, "fixed_expenses": 40694.65, - "running_cost": 6080.0 + "running_cost": 6080 }, { "month": 4, - "income": 11040.0, + "income": 11040, "fixed_expenses": 42148.15, "running_cost": 17682.56 }, { "month": 5, - "income": 41952.0, + "income": 41952, "fixed_expenses": 42245.05, "running_cost": 734.27 }, { "month": 6, - "income": 42366.0, + "income": 42366, "fixed_expenses": 42212.75, "running_cost": 79.9 }, { "month": 7, - "income": 58788.0, + "income": 58788, "fixed_expenses": 41760.55, "running_cost": 6239.68 }, { "month": 8, - "income": 41124.0, + "income": 41124, "fixed_expenses": 42341.95, "running_cost": 958.64 }, @@ -949,19 +949,19 @@ "month": 2, "income": 60504.37499999999, "fixed_expenses": 57630.369999999995, - "running_cost": 322.0 + "running_cost": 322 }, { "month": 3, "income": 77961.375, "fixed_expenses": 58171.72, - "running_cost": 1913.0 + "running_cost": 1913 }, { "month": 4, "income": 59512.5, "fixed_expenses": 56079.97, - "running_cost": 2277.0 + "running_cost": 2277 }, { "month": 5, @@ -1039,7 +1039,7 @@ "month": 4, "income": 58719.009999999995, "fixed_expenses": 34579.41, - "running_cost": 2277.0 + "running_cost": 2277 }, { "month": 5, @@ -1109,25 +1109,25 @@ }, { "month": 3, - "income": 81995.0, + "income": 81995, "fixed_expenses": 38464.09, "running_cost": 19503.96 }, { "month": 4, - "income": 69736.0, + "income": 69736, "fixed_expenses": 50398.09, - "running_cost": 857.0 + "running_cost": 857 }, { "month": 5, - "income": 64538.0, + "income": 64538, "fixed_expenses": 51894.09, "running_cost": 23019.55 }, { "month": 6, - "income": 60260.0, + "income": 60260, "fixed_expenses": 52217.09, "running_cost": 19078.5 }, @@ -1205,13 +1205,13 @@ }, { "month": 6, - "income": 47840.0, + "income": 47840, "fixed_expenses": 36418.25, "running_cost": 0 }, { "month": 7, - "income": 34684.0, + "income": 34684, "fixed_expenses": 35190.85, "running_cost": 38297.25 }, @@ -1219,7 +1219,7 @@ "month": 8, "income": 0, "fixed_expenses": 36192.149999999994, - "running_cost": 16000.0 + "running_cost": 16000 }, { "month": 9, @@ -1271,7 +1271,7 @@ }, { "month": 4, - "income": 42159.0, + "income": 42159, "fixed_expenses": 58411.619999999995, "running_cost": 0 }, @@ -1283,15 +1283,15 @@ }, { "month": 6, - "income": 55016.0, + "income": 55016, "fixed_expenses": 62626.77, "running_cost": 0 }, { "month": 7, - "income": 61594.0, + "income": 61594, "fixed_expenses": 59994.31999999999, - "running_cost": 2070.0 + "running_cost": 2070 }, { "month": 8, @@ -1351,11 +1351,11 @@ "month": 4, "income": 43642.5, "fixed_expenses": 20863.059999999998, - "running_cost": 1586.0 + "running_cost": 1586 }, { "month": 5, - "income": 41262.0, + "income": 41262, "fixed_expenses": 19667.96, "running_cost": 2392.835 }, @@ -1363,7 +1363,7 @@ "month": 6, "income": 40600.75, "fixed_expenses": 20281.66, - "running_cost": 429.0 + "running_cost": 429 }, { "month": 7, @@ -1373,7 +1373,7 @@ }, { "month": 8, - "income": 41791.0, + "income": 41791, "fixed_expenses": 20669.260000000002, "running_cost": 0 }, @@ -1429,7 +1429,7 @@ "month": 4, "income": 33723.75, "fixed_expenses": 13686.439999999999, - "running_cost": 6239.0 + "running_cost": 6239 }, { "month": 5, @@ -1439,7 +1439,7 @@ }, { "month": 6, - "income": 40733.0, + "income": 40733, "fixed_expenses": 18418.39, "running_cost": 60306.630000000005 }, @@ -1566,80 +1566,80 @@ { "month": 1, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 2, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 3, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 4, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 5, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 6, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 7, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 8, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 9, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 10, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 11, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 }, { "month": 12, "income": 0, - "fixed_expenses": 20125.0, + "fixed_expenses": 20125, "running_cost": 0 } ] }, { "machine": "ELB 1TON RV", - "code": "", + "code": "RV02", "data": [ { "month": 1, @@ -1717,7 +1717,7 @@ }, { "machine": "10 KVA GENERATOR", - "code": "", + "code": "GN100", "data": [ { "month": 1, @@ -1795,7 +1795,7 @@ }, { "machine": "7,5 KVA GENERATOR", - "code": "", + "code": "GN075", "data": [ { "month": 1, @@ -1873,7 +1873,7 @@ }, { "machine": "TRAILOR PLANT", - "code": "", + "code": "TR01", "data": [ { "month": 1, @@ -1951,7 +1951,7 @@ }, { "machine": "DIESEL TRAILOR", - "code": "", + "code": "TR02", "data": [ { "month": 1, @@ -1969,7 +1969,7 @@ "month": 3, "income": 0, "fixed_expenses": 804.9999999999999, - "running_cost": 885.0 + "running_cost": 885 }, { "month": 4, @@ -2107,7 +2107,7 @@ }, { "machine": "OFFICE CONTAINER #1", - "code": "", + "code": "OC01", "data": [ { "month": 1, @@ -2123,37 +2123,37 @@ }, { "month": 3, - "income": 1725.0, + "income": 1725, "fixed_expenses": 4360.416666666666, "running_cost": 0 }, { "month": 4, - "income": 1725.0, + "income": 1725, "fixed_expenses": 4360.416666666666, "running_cost": 0 }, { "month": 5, - "income": 1725.0, + "income": 1725, "fixed_expenses": 4360.416666666666, "running_cost": 0 }, { "month": 6, - "income": 1725.0, + "income": 1725, "fixed_expenses": 4360.416666666666, "running_cost": 0 }, { "month": 7, - "income": 1725.0, + "income": 1725, "fixed_expenses": 4360.416666666666, "running_cost": 0 }, { "month": 8, - "income": 1725.0, + "income": 1725, "fixed_expenses": 4360.416666666666, "running_cost": 0 }, @@ -2185,7 +2185,7 @@ }, { "machine": "OFFICE CONTAINER #2", - "code": "", + "code": "OC02", "data": [ { "month": 1, @@ -2201,37 +2201,37 @@ }, { "month": 3, - "income": 1725.0, + "income": 1725, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 4, - "income": 1725.0, + "income": 1725, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 5, - "income": 1725.0, + "income": 1725, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 6, - "income": 1725.0, + "income": 1725, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 7, - "income": 1725.0, + "income": 1725, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 8, - "income": 1725.0, + "income": 1725, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, @@ -2262,8 +2262,8 @@ ] }, { - "machine": "SITE CONTANER #1", - "code": "", + "machine": "Site Container #1", + "code": "SC01", "data": [ { "month": 1, @@ -2273,43 +2273,43 @@ }, { "month": 2, - "income": 1380.0, + "income": 1380, "fixed_expenses": 1878.333333333333, "running_cost": 0 }, { "month": 3, - "income": 1380.0, + "income": 1380, "fixed_expenses": 1878.333333333333, "running_cost": 0 }, { "month": 4, - "income": 1380.0, + "income": 1380, "fixed_expenses": 1878.333333333333, "running_cost": 0 }, { "month": 5, - "income": 1380.0, + "income": 1380, "fixed_expenses": 1878.333333333333, "running_cost": 0 }, { "month": 6, - "income": 1380.0, + "income": 1380, "fixed_expenses": 1878.333333333333, "running_cost": 0 }, { "month": 7, - "income": 1380.0, + "income": 1380, "fixed_expenses": 1878.333333333333, "running_cost": 0 }, { "month": 8, - "income": 1380.0, + "income": 1380, "fixed_expenses": 1878.333333333333, "running_cost": 0 }, @@ -2340,8 +2340,8 @@ ] }, { - "machine": "SITE CONTANER #2", - "code": "", + "machine": "Site Container #2", + "code": "SC02", "data": [ { "month": 1, @@ -2351,43 +2351,43 @@ }, { "month": 2, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 3, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 4, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 5, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 6, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 7, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 8, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, @@ -2418,8 +2418,8 @@ ] }, { - "machine": "SITE CONTANER #3", - "code": "", + "machine": "Site Container #3", + "code": "SC03", "data": [ { "month": 1, @@ -2429,43 +2429,43 @@ }, { "month": 2, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 3, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 4, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 5, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 6, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 7, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 8, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, @@ -2496,8 +2496,8 @@ ] }, { - "machine": "SITE CONTANER #4", - "code": "", + "machine": "Site Container #4", + "code": "SC04", "data": [ { "month": 1, @@ -2507,43 +2507,43 @@ }, { "month": 2, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 3, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 4, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 5, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 6, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 7, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 8, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, @@ -2574,8 +2574,8 @@ ] }, { - "machine": "SITE CONTANER #5", - "code": "", + "machine": "Site Container #5", + "code": "SC05", "data": [ { "month": 1, @@ -2585,43 +2585,43 @@ }, { "month": 2, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 3, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 4, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 5, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 6, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 7, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 8, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, @@ -2652,8 +2652,8 @@ ] }, { - "machine": "SITE CONTANER #6", - "code": "", + "machine": "Site Container #6", + "code": "SC06", "data": [ { "month": 1, @@ -2663,43 +2663,43 @@ }, { "month": 2, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 3, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 4, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 5, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 6, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 7, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, { "month": 8, - "income": 1380.0, + "income": 1380, "fixed_expenses": 2347.9166666666665, "running_cost": 0 }, @@ -2730,8 +2730,8 @@ ] }, { - "machine": "SITE CONTANER #7", - "code": "", + "machine": "Site Container #7", + "code": "SC07", "data": [ { "month": 1, @@ -2741,43 +2741,43 @@ }, { "month": 2, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 3, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 4, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 5, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 6, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 7, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, { "month": 8, - "income": 1380.0, + "income": 1380, "fixed_expenses": 3018.749999999999, "running_cost": 0 }, @@ -2809,7 +2809,7 @@ }, { "machine": "Porta camp #1", - "code": "", + "code": "PC01", "data": [ { "month": 1, @@ -2887,7 +2887,7 @@ }, { "machine": "Porta camp #2", - "code": "", + "code": "PC02", "data": [ { "month": 1, @@ -2965,7 +2965,7 @@ }, { "machine": "Solar office", - "code": "", + "code": "SO01", "data": [ { "month": 1, @@ -3043,7 +3043,7 @@ }, { "machine": "30KVA GENERATOR", - "code": "", + "code": "GN300", "data": [ { "month": 1, diff --git a/data/source/mrc.json b/data/source/mrc.json index 0dc893d..a6ae21b 100644 --- a/data/source/mrc.json +++ b/data/source/mrc.json @@ -1 +1,716 @@ -[{"name": "Terex TLB 820", "code": "TLB01", "month": 1, "amount": 1897.5}, {"name": "Terex TLB 820", "code": "TLB01", "month": 2, "amount": 37327.25}, {"name": "Terex TLB 820", "code": "TLB01", "month": 3, "amount": 3160.1}, {"name": "Terex TLB 820", "code": "TLB01", "month": 4, "amount": 451.05}, {"name": "Terex TLB 820", "code": "TLB01", "month": 7, "amount": 28181.52}, {"name": "Terex TLB 820", "code": "TLB01", "month": 8, "amount": 2195.47}, {"name": "CAT 425F (428E)", "code": "TLB02", "month": 3, "amount": 7374.0}, {"name": "CAT 425F (428E)", "code": "TLB02", "month": 4, "amount": 13096.9}, {"name": "CAT 425F (428E)", "code": "TLB02", "month": 5, "amount": 25176.9}, {"name": "CAT 425F (428E)", "code": "TLB02", "month": 6, "amount": 956.02}, {"name": "CAT 425F (428E)", "code": "TLB02", "month": 7, "amount": 36443.5}, {"name": "CAT 425F (428E)", "code": "TLB02", "month": 8, "amount": 78491.54}, {"name": "Terex TLB 820", "code": "TLB03", "month": 1, "amount": 9776.34}, {"name": "Terex TLB 820", "code": "TLB03", "month": 2, "amount": 1655.8}, {"name": "Terex TLB 820", "code": "TLB03", "month": 4, "amount": 767.86}, {"name": "Terex TLB 820", "code": "TLB03", "month": 5, "amount": 9313.119999999999}, {"name": "Terex TLB 820", "code": "TLB03", "month": 6, "amount": 21383.09}, {"name": "Terex TLB 820", "code": "TLB03", "month": 7, "amount": 33024.55}, {"name": "Terex TLB 820", "code": "TLB03", "month": 8, "amount": 4720.14}, {"name": "BELL TLB", "code": "TLB04", "month": 1, "amount": 1473.7199999999998}, {"name": "BELL TLB", "code": "TLB04", "month": 2, "amount": 12790.3}, {"name": "BELL TLB", "code": "TLB04", "month": 3, "amount": 15922.25}, {"name": "BELL TLB", "code": "TLB04", "month": 5, "amount": 7030.56}, {"name": "BELL TLB", "code": "TLB04", "month": 6, "amount": 3800.0}, {"name": "BELL TLB", "code": "TLB04", "month": 8, "amount": 2109.95}, {"name": "CASE TLB", "code": "TLB05", "month": 1, "amount": 3371.61}, {"name": "CASE TLB", "code": "TLB05", "month": 2, "amount": 1439.0}, {"name": "CASE TLB", "code": "TLB05", "month": 3, "amount": 2422.94}, {"name": "CASE TLB", "code": "TLB05", "month": 4, "amount": 4887.85}, {"name": "CASE TLB", "code": "TLB05", "month": 5, "amount": 248.0}, {"name": "CASE TLB", "code": "TLB05", "month": 6, "amount": 1052.92}, {"name": "CASE TLB", "code": "TLB05", "month": 7, "amount": 5000.35}, {"name": "CASE TLB", "code": "TLB06", "month": 1, "amount": 3315.0}, {"name": "CASE TLB", "code": "TLB06", "month": 2, "amount": 7374.33}, {"name": "CASE TLB", "code": "TLB06", "month": 4, "amount": 4875.93}, {"name": "CASE TLB", "code": "TLB06", "month": 5, "amount": 7877.5}, {"name": "CASE TLB", "code": "TLB06", "month": 7, "amount": 15522.85}, {"name": "CASE TLB", "code": "TLB07", "month": 1, "amount": 6078.4400000000005}, {"name": "CASE TLB", "code": "TLB07", "month": 2, "amount": 25788.62}, {"name": "CASE TLB", "code": "TLB07", "month": 3, "amount": 2580.19}, {"name": "CASE TLB", "code": "TLB07", "month": 4, "amount": 346.0}, {"name": "CASE TLB", "code": "TLB07", "month": 5, "amount": 2383.56}, {"name": "CASE TLB", "code": "TLB07", "month": 7, "amount": 5000.35}, {"name": "CASE TLB", "code": "TLB07", "month": 8, "amount": 1644.5}, {"name": "CASE420 (430)", "code": "SS01", "month": 1, "amount": 1378.815}, {"name": "CASE420 (430)", "code": "SS01", "month": 2, "amount": 2794.5}, {"name": "CASE420 (430)", "code": "SS01", "month": 3, "amount": 3226.15}, {"name": "CASE420 (430)", "code": "SS01", "month": 5, "amount": 1417.57}, {"name": "CASE420 (430)", "code": "SS01", "month": 8, "amount": 1482.35}, {"name": "CASE420 (430)", "code": "SS02", "month": 1, "amount": 9332.215}, {"name": "CASE420 (430)", "code": "SS02", "month": 2, "amount": 2794.5}, {"name": "CASE420 (430)", "code": "SS02", "month": 5, "amount": 1417.57}, {"name": "CASE420 (430)", "code": "SS02", "month": 7, "amount": 3225.0}, {"name": "CASE420 (430)", "code": "SS02", "month": 8, "amount": 6118.0}, {"name": "CAT 2,4TON RV", "code": "RV01", "month": 1, "amount": 2657.58}, {"name": "CAT 2,4TON RV", "code": "RV01", "month": 2, "amount": 460.0}, {"name": "CAT 2,4TON RV", "code": "RV01", "month": 4, "amount": 890.0}, {"name": "CAT 2,4TON RV", "code": "RV01", "month": 5, "amount": 4245.0}, {"name": "CAT 2,4TON RV", "code": "RV01", "month": 6, "amount": 7772.469999999999}, {"name": "CAT 2,4TON RV", "code": "RV01", "month": 8, "amount": 4209.71}, {"name": "CAT 12TON RV", "code": "RV03", "month": 3, "amount": 110.0}, {"name": "CAT 12TON RV", "code": "RV03", "month": 5, "amount": 962.63}, {"name": "CAT 12TON RV", "code": "RV03", "month": 7, "amount": 3219.0099999999998}, {"name": "Case Compactor", "code": "RV04", "month": 1, "amount": 2279.85}, {"name": "Case Compactor", "code": "RV04", "month": 2, "amount": 3890.0}, {"name": "Case Compactor", "code": "RV04", "month": 3, "amount": 6080.0}, {"name": "Case Compactor", "code": "RV04", "month": 4, "amount": 17682.56}, {"name": "Case Compactor", "code": "RV04", "month": 5, "amount": 734.27}, {"name": "Case Compactor", "code": "RV04", "month": 6, "amount": 79.9}, {"name": "Case Compactor", "code": "RV04", "month": 7, "amount": 6239.68}, {"name": "Case Compactor", "code": "RV04", "month": 8, "amount": 958.64}, {"name": "DEZZI GRADER", "code": "MG01", "month": 1, "amount": 22620.53}, {"name": "DEZZI GRADER", "code": "MG01", "month": 2, "amount": 322.0}, {"name": "DEZZI GRADER", "code": "MG01", "month": 3, "amount": 1913.0}, {"name": "DEZZI GRADER", "code": "MG01", "month": 4, "amount": 2277.0}, {"name": "DEZZI GRADER", "code": "MG01", "month": 5, "amount": 3940.735}, {"name": "DEZZI GRADER", "code": "MG01", "month": 6, "amount": 29262.329999999998}, {"name": "DEZZI GRADER", "code": "MG01", "month": 7, "amount": 4497.1900000000005}, {"name": "DEZZI GRADER", "code": "MG01", "month": 8, "amount": 9667.905}, {"name": "DEZZI GRADER", "code": "MG02", "month": 2, "amount": 4342.4}, {"name": "DEZZI GRADER", "code": "MG02", "month": 3, "amount": 4959.95}, {"name": "DEZZI GRADER", "code": "MG02", "month": 4, "amount": 2277.0}, {"name": "DEZZI GRADER", "code": "MG02", "month": 5, "amount": 6375.285}, {"name": "DEZZI GRADER", "code": "MG02", "month": 6, "amount": 22787.25}, {"name": "DEZZI GRADER", "code": "MG02", "month": 7, "amount": 4991.6900000000005}, {"name": "DEZZI GRADER", "code": "MG02", "month": 8, "amount": 8074.575000000001}, {"name": "ISUZU 800", "code": "CT01", "month": 3, "amount": 19503.96}, {"name": "ISUZU 800", "code": "CT01", "month": 4, "amount": 857.0}, {"name": "ISUZU 800", "code": "CT01", "month": 5, "amount": 23019.55}, {"name": "ISUZU 800", "code": "CT01", "month": 6, "amount": 19078.5}, {"name": "TATTA 6M3", "code": "TP601", "month": 3, "amount": 2502.2}, {"name": "TATTA 6M3", "code": "TP601", "month": 4, "amount": 4300.95}, {"name": "TATTA 6M3", "code": "TP601", "month": 5, "amount": 349.2}, {"name": "TATTA 6M3", "code": "TP601", "month": 7, "amount": 38297.25}, {"name": "TATTA 6M3", "code": "TP601", "month": 8, "amount": 16000.0}, {"name": "TATTA 6M3", "code": "TP602", "month": 1, "amount": 1634.5}, {"name": "TATTA 6M3", "code": "TP602", "month": 3, "amount": 2502.2}, {"name": "TATTA 6M3", "code": "TP602", "month": 7, "amount": 2070.0}, {"name": "TATTA 6M3", "code": "TP602", "month": 8, "amount": 5108.76}, {"name": "MERC", "code": "WB01", "month": 2, "amount": 2330.3}, {"name": "MERC", "code": "WB01", "month": 4, "amount": 1586.0}, {"name": "MERC", "code": "WB01", "month": 5, "amount": 2392.835}, {"name": "MERC", "code": "WB01", "month": 6, "amount": 429.0}, {"name": "MERC", "code": "WB02", "month": 2, "amount": 88147.86}, {"name": "MERC", "code": "WB02", "month": 3, "amount": 2472.55}, {"name": "MERC", "code": "WB02", "month": 4, "amount": 6239.0}, {"name": "MERC", "code": "WB02", "month": 5, "amount": 2868.6349999999998}, {"name": "MERC", "code": "WB02", "month": 6, "amount": 60306.630000000005}, {"name": "MERC", "code": "WB02", "month": 8, "amount": 4186.58}, {"name": "UD 80", "code": "DT01", "month": 1, "amount": 2606.82}, {"name": "UD 80", "code": "DT01", "month": 4, "amount": 14399.210000000001}, {"name": "UD 80", "code": "DT01", "month": 5, "amount": 6402.19}, {"name": "UD 80", "code": "DT01", "month": 6, "amount": 47080.54}, {"name": "UD 80", "code": "DT01", "month": 7, "amount": 799.25}, {"name": "UD 80", "code": "DT01", "month": 8, "amount": 6402.19}, {"name": "DIESEL TRAILOR", "code": "DB01", "month": 1, "amount": 3113.9}, {"name": "DIESEL TRAILOR", "code": "DB01", "month": 3, "amount": 885.0}, {"name": "DIESEL TRAILOR", "code": "DB01", "month": 7, "amount": 7119.62}, {"name": "WATER TRAILOR", "code": "WT02", "month": 6, "amount": 395.4}] \ No newline at end of file +[ + { + "name": "Terex TLB 820", + "code": "TLB01", + "month": 1, + "amount": 1897.5 + }, + { + "name": "Terex TLB 820", + "code": "TLB01", + "month": 2, + "amount": 37327.25 + }, + { + "name": "Terex TLB 820", + "code": "TLB01", + "month": 3, + "amount": 3160.1 + }, + { + "name": "Terex TLB 820", + "code": "TLB01", + "month": 4, + "amount": 451.05 + }, + { + "name": "Terex TLB 820", + "code": "TLB01", + "month": 7, + "amount": 28181.52 + }, + { + "name": "Terex TLB 820", + "code": "TLB01", + "month": 8, + "amount": 2195.47 + }, + { + "name": "CAT 425F (428E)", + "code": "TLB02", + "month": 3, + "amount": 7374 + }, + { + "name": "CAT 425F (428E)", + "code": "TLB02", + "month": 4, + "amount": 13096.9 + }, + { + "name": "CAT 425F (428E)", + "code": "TLB02", + "month": 5, + "amount": 25176.9 + }, + { + "name": "CAT 425F (428E)", + "code": "TLB02", + "month": 6, + "amount": 956.02 + }, + { + "name": "CAT 425F (428E)", + "code": "TLB02", + "month": 7, + "amount": 36443.5 + }, + { + "name": "CAT 425F (428E)", + "code": "TLB02", + "month": 8, + "amount": 78491.54 + }, + { + "name": "Terex TLB 820", + "code": "TLB03", + "month": 1, + "amount": 9776.34 + }, + { + "name": "Terex TLB 820", + "code": "TLB03", + "month": 2, + "amount": 1655.8 + }, + { + "name": "Terex TLB 820", + "code": "TLB03", + "month": 4, + "amount": 767.86 + }, + { + "name": "Terex TLB 820", + "code": "TLB03", + "month": 5, + "amount": 9313.119999999999 + }, + { + "name": "Terex TLB 820", + "code": "TLB03", + "month": 6, + "amount": 21383.09 + }, + { + "name": "Terex TLB 820", + "code": "TLB03", + "month": 7, + "amount": 33024.55 + }, + { + "name": "Terex TLB 820", + "code": "TLB03", + "month": 8, + "amount": 4720.14 + }, + { + "name": "BELL TLB", + "code": "TLB04", + "month": 1, + "amount": 1473.7199999999998 + }, + { + "name": "BELL TLB", + "code": "TLB04", + "month": 2, + "amount": 12790.3 + }, + { + "name": "BELL TLB", + "code": "TLB04", + "month": 3, + "amount": 15922.25 + }, + { + "name": "BELL TLB", + "code": "TLB04", + "month": 5, + "amount": 7030.56 + }, + { + "name": "BELL TLB", + "code": "TLB04", + "month": 6, + "amount": 3800 + }, + { + "name": "BELL TLB", + "code": "TLB04", + "month": 8, + "amount": 2109.95 + }, + { + "name": "CASE TLB", + "code": "TLB05", + "month": 1, + "amount": 3371.61 + }, + { + "name": "CASE TLB", + "code": "TLB05", + "month": 2, + "amount": 1439 + }, + { + "name": "CASE TLB", + "code": "TLB05", + "month": 3, + "amount": 2422.94 + }, + { + "name": "CASE TLB", + "code": "TLB05", + "month": 4, + "amount": 4887.85 + }, + { + "name": "CASE TLB", + "code": "TLB05", + "month": 5, + "amount": 248 + }, + { + "name": "CASE TLB", + "code": "TLB05", + "month": 6, + "amount": 1052.92 + }, + { + "name": "CASE TLB", + "code": "TLB05", + "month": 7, + "amount": 5000.35 + }, + { + "name": "CASE TLB", + "code": "TLB06", + "month": 1, + "amount": 3315 + }, + { + "name": "CASE TLB", + "code": "TLB06", + "month": 2, + "amount": 7374.33 + }, + { + "name": "CASE TLB", + "code": "TLB06", + "month": 4, + "amount": 4875.93 + }, + { + "name": "CASE TLB", + "code": "TLB06", + "month": 5, + "amount": 7877.5 + }, + { + "name": "CASE TLB", + "code": "TLB06", + "month": 7, + "amount": 15522.85 + }, + { + "name": "CASE TLB", + "code": "TLB07", + "month": 1, + "amount": 6078.4400000000005 + }, + { + "name": "CASE TLB", + "code": "TLB07", + "month": 2, + "amount": 25788.62 + }, + { + "name": "CASE TLB", + "code": "TLB07", + "month": 3, + "amount": 2580.19 + }, + { + "name": "CASE TLB", + "code": "TLB07", + "month": 4, + "amount": 346 + }, + { + "name": "CASE TLB", + "code": "TLB07", + "month": 5, + "amount": 2383.56 + }, + { + "name": "CASE TLB", + "code": "TLB07", + "month": 7, + "amount": 5000.35 + }, + { + "name": "CASE TLB", + "code": "TLB07", + "month": 8, + "amount": 1644.5 + }, + { + "name": "CASE420 (430)", + "code": "SS01", + "month": 1, + "amount": 1378.815 + }, + { + "name": "CASE420 (430)", + "code": "SS01", + "month": 2, + "amount": 2794.5 + }, + { + "name": "CASE420 (430)", + "code": "SS01", + "month": 3, + "amount": 3226.15 + }, + { + "name": "CASE420 (430)", + "code": "SS01", + "month": 5, + "amount": 1417.57 + }, + { + "name": "CASE420 (430)", + "code": "SS01", + "month": 8, + "amount": 1482.35 + }, + { + "name": "CASE420 (430)", + "code": "SS02", + "month": 1, + "amount": 9332.215 + }, + { + "name": "CASE420 (430)", + "code": "SS02", + "month": 2, + "amount": 2794.5 + }, + { + "name": "CASE420 (430)", + "code": "SS02", + "month": 5, + "amount": 1417.57 + }, + { + "name": "CASE420 (430)", + "code": "SS02", + "month": 7, + "amount": 3225 + }, + { + "name": "CASE420 (430)", + "code": "SS02", + "month": 8, + "amount": 6118 + }, + { + "name": "CAT 2,4TON RV", + "code": "RV01", + "month": 1, + "amount": 2657.58 + }, + { + "name": "CAT 2,4TON RV", + "code": "RV01", + "month": 2, + "amount": 460 + }, + { + "name": "CAT 2,4TON RV", + "code": "RV01", + "month": 4, + "amount": 890 + }, + { + "name": "CAT 2,4TON RV", + "code": "RV01", + "month": 5, + "amount": 4245 + }, + { + "name": "CAT 2,4TON RV", + "code": "RV01", + "month": 6, + "amount": 7772.469999999999 + }, + { + "name": "CAT 2,4TON RV", + "code": "RV01", + "month": 8, + "amount": 4209.71 + }, + { + "name": "CAT 12TON RV", + "code": "RV03", + "month": 3, + "amount": 110 + }, + { + "name": "CAT 12TON RV", + "code": "RV03", + "month": 5, + "amount": 962.63 + }, + { + "name": "CAT 12TON RV", + "code": "RV03", + "month": 7, + "amount": 3219.0099999999998 + }, + { + "name": "Case Compactor", + "code": "RV04", + "month": 1, + "amount": 2279.85 + }, + { + "name": "Case Compactor", + "code": "RV04", + "month": 2, + "amount": 3890 + }, + { + "name": "Case Compactor", + "code": "RV04", + "month": 3, + "amount": 6080 + }, + { + "name": "Case Compactor", + "code": "RV04", + "month": 4, + "amount": 17682.56 + }, + { + "name": "Case Compactor", + "code": "RV04", + "month": 5, + "amount": 734.27 + }, + { + "name": "Case Compactor", + "code": "RV04", + "month": 6, + "amount": 79.9 + }, + { + "name": "Case Compactor", + "code": "RV04", + "month": 7, + "amount": 6239.68 + }, + { + "name": "Case Compactor", + "code": "RV04", + "month": 8, + "amount": 958.64 + }, + { + "name": "DEZZI GRADER", + "code": "MG01", + "month": 1, + "amount": 22620.53 + }, + { + "name": "DEZZI GRADER", + "code": "MG01", + "month": 2, + "amount": 322 + }, + { + "name": "DEZZI GRADER", + "code": "MG01", + "month": 3, + "amount": 1913 + }, + { + "name": "DEZZI GRADER", + "code": "MG01", + "month": 4, + "amount": 2277 + }, + { + "name": "DEZZI GRADER", + "code": "MG01", + "month": 5, + "amount": 3940.735 + }, + { + "name": "DEZZI GRADER", + "code": "MG01", + "month": 6, + "amount": 29262.329999999998 + }, + { + "name": "DEZZI GRADER", + "code": "MG01", + "month": 7, + "amount": 4497.1900000000005 + }, + { + "name": "DEZZI GRADER", + "code": "MG01", + "month": 8, + "amount": 9667.905 + }, + { + "name": "DEZZI GRADER", + "code": "MG02", + "month": 2, + "amount": 4342.4 + }, + { + "name": "DEZZI GRADER", + "code": "MG02", + "month": 3, + "amount": 4959.95 + }, + { + "name": "DEZZI GRADER", + "code": "MG02", + "month": 4, + "amount": 2277 + }, + { + "name": "DEZZI GRADER", + "code": "MG02", + "month": 5, + "amount": 6375.285 + }, + { + "name": "DEZZI GRADER", + "code": "MG02", + "month": 6, + "amount": 22787.25 + }, + { + "name": "DEZZI GRADER", + "code": "MG02", + "month": 7, + "amount": 4991.6900000000005 + }, + { + "name": "DEZZI GRADER", + "code": "MG02", + "month": 8, + "amount": 8074.575000000001 + }, + { + "name": "ISUZU 800", + "code": "CT01", + "month": 3, + "amount": 19503.96 + }, + { + "name": "ISUZU 800", + "code": "CT01", + "month": 4, + "amount": 857 + }, + { + "name": "ISUZU 800", + "code": "CT01", + "month": 5, + "amount": 23019.55 + }, + { + "name": "ISUZU 800", + "code": "CT01", + "month": 6, + "amount": 19078.5 + }, + { + "name": "TATTA 6M3", + "code": "TP601", + "month": 3, + "amount": 2502.2 + }, + { + "name": "TATTA 6M3", + "code": "TP601", + "month": 4, + "amount": 4300.95 + }, + { + "name": "TATTA 6M3", + "code": "TP601", + "month": 5, + "amount": 349.2 + }, + { + "name": "TATTA 6M3", + "code": "TP601", + "month": 7, + "amount": 38297.25 + }, + { + "name": "TATTA 6M3", + "code": "TP601", + "month": 8, + "amount": 16000 + }, + { + "name": "TATTA 6M3", + "code": "TP602", + "month": 1, + "amount": 1634.5 + }, + { + "name": "TATTA 6M3", + "code": "TP602", + "month": 3, + "amount": 2502.2 + }, + { + "name": "TATTA 6M3", + "code": "TP602", + "month": 7, + "amount": 2070 + }, + { + "name": "TATTA 6M3", + "code": "TP602", + "month": 8, + "amount": 5108.76 + }, + { + "name": "MERC", + "code": "WB01", + "month": 2, + "amount": 2330.3 + }, + { + "name": "MERC", + "code": "WB01", + "month": 4, + "amount": 1586 + }, + { + "name": "MERC", + "code": "WB01", + "month": 5, + "amount": 2392.835 + }, + { + "name": "MERC", + "code": "WB01", + "month": 6, + "amount": 429 + }, + { + "name": "MERC", + "code": "WB02", + "month": 2, + "amount": 88147.86 + }, + { + "name": "MERC", + "code": "WB02", + "month": 3, + "amount": 2472.55 + }, + { + "name": "MERC", + "code": "WB02", + "month": 4, + "amount": 6239 + }, + { + "name": "MERC", + "code": "WB02", + "month": 5, + "amount": 2868.6349999999998 + }, + { + "name": "MERC", + "code": "WB02", + "month": 6, + "amount": 60306.630000000005 + }, + { + "name": "MERC", + "code": "WB02", + "month": 8, + "amount": 4186.58 + }, + { + "name": "UD 80", + "code": "DT01", + "month": 1, + "amount": 2606.82 + }, + { + "name": "UD 80", + "code": "DT01", + "month": 4, + "amount": 14399.210000000001 + }, + { + "name": "UD 80", + "code": "DT01", + "month": 5, + "amount": 6402.19 + }, + { + "name": "UD 80", + "code": "DT01", + "month": 6, + "amount": 47080.54 + }, + { + "name": "UD 80", + "code": "DT01", + "month": 7, + "amount": 799.25 + }, + { + "name": "UD 80", + "code": "DT01", + "month": 8, + "amount": 6402.19 + }, + { + "name": "DIESEL TRAILOR", + "code": "DB01", + "month": 1, + "amount": 3113.9 + }, + { + "name": "DIESEL TRAILOR", + "code": "DB01", + "month": 3, + "amount": 885 + }, + { + "name": "DIESEL TRAILOR", + "code": "DB01", + "month": 7, + "amount": 7119.62 + }, + { + "name": "WATER TRAILOR", + "code": "WT02", + "month": 6, + "amount": 395.4 + } +] \ No newline at end of file diff --git a/data/source/overheads.json b/data/source/overheads.json index 308ab82..491c638 100644 --- a/data/source/overheads.json +++ b/data/source/overheads.json @@ -6,7 +6,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 29566.0, + "amount_excl": 29566, "vat": 4434.899999999994, "amount_incl": 34000.899999999994 }, @@ -17,7 +17,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 43750.0, + "amount_excl": 43750, "vat": 6562.499999999993, "amount_incl": 50312.49999999999 }, @@ -61,9 +61,9 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 17500.0, - "vat": 2625.0, - "amount_incl": 20125.0 + "amount_excl": 17500, + "vat": 2625, + "amount_incl": 20125 }, { "section": "Plant Overheads", @@ -72,7 +72,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 8750.0, + "amount_excl": 8750, "vat": 1312.5, "amount_incl": 10062.5 }, @@ -83,7 +83,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 8750.0, + "amount_excl": 8750, "vat": 1312.5, "amount_incl": 10062.5 }, @@ -94,9 +94,9 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 17500.0, - "vat": 2625.0, - "amount_incl": 20125.0 + "amount_excl": 17500, + "vat": 2625, + "amount_incl": 20125 }, { "section": "Plant Overheads", @@ -149,9 +149,9 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 17500.0, - "vat": 2625.0, - "amount_incl": 20125.0 + "amount_excl": 17500, + "vat": 2625, + "amount_incl": 20125 }, { "section": "Plant Overheads", @@ -193,7 +193,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 3500.0, + "amount_excl": 3500, "vat": 524.9999999999995, "amount_incl": 4024.9999999999995 }, @@ -215,7 +215,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 455.0, + "amount_excl": 455, "vat": 68.25, "amount_incl": 523.25 }, @@ -237,7 +237,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 700.0, + "amount_excl": 700, "vat": 104.99999999999989, "amount_incl": 804.9999999999999 }, @@ -248,7 +248,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 875.0, + "amount_excl": 875, "vat": 131.2499999999999, "amount_incl": 1006.2499999999999 }, @@ -358,7 +358,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 1750.0, + "amount_excl": 1750, "vat": 262.4999999999998, "amount_incl": 2012.4999999999998 }, @@ -369,7 +369,7 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 1750.0, + "amount_excl": 1750, "vat": 262.4999999999998, "amount_incl": 2012.4999999999998 }, @@ -414,7 +414,7 @@ "qty": 0, "unit_price": 0, "amount_excl": 1033.2916666666667, - "vat": 0.0, + "vat": 0, "amount_incl": 1033.2916666666667 }, { @@ -425,7 +425,7 @@ "qty": 0, "unit_price": 0, "amount_excl": 3318.1416666666664, - "vat": 0.0, + "vat": 0, "amount_incl": 3318.1416666666664 }, { @@ -436,7 +436,7 @@ "qty": 0, "unit_price": 0, "amount_excl": 29036.08, - "vat": 0.0, + "vat": 0, "amount_incl": 29036.08 }, { @@ -446,9 +446,9 @@ "supplier": "Bastion", "qty": 0, "unit_price": 0, - "amount_excl": 29819.0, - "vat": 0.0, - "amount_incl": 29819.0 + "amount_excl": 29819, + "vat": 0, + "amount_incl": 29819 }, { "section": "Bastion Finance", @@ -458,7 +458,7 @@ "qty": 0, "unit_price": 0, "amount_excl": 19088.95, - "vat": 0.0, + "vat": 0, "amount_incl": 19088.95 }, { @@ -468,9 +468,9 @@ "supplier": "Bastion", "qty": 0, "unit_price": 0, - "amount_excl": 28641.0, + "amount_excl": 28641, "vat": 0, - "amount_incl": 28641.0 + "amount_incl": 28641 }, { "section": "Bastion Finance", @@ -571,6 +571,39 @@ "vat": 32.536500000000004, "amount_incl": 249.44650000000001 }, + { + "section": "Personnel", + "description": "Attie", + "gl_account": "", + "supplier": "", + "qty": 0, + "unit_price": 0, + "amount_excl": 0, + "vat": 0, + "amount_incl": 0 + }, + { + "section": "Personnel", + "description": "Sandrea", + "gl_account": "", + "supplier": "", + "qty": 0, + "unit_price": 0, + "amount_excl": 0, + "vat": 0, + "amount_incl": 0 + }, + { + "section": "Personnel", + "description": "Jantjie", + "gl_account": "", + "supplier": "", + "qty": 0, + "unit_price": 0, + "amount_excl": 0, + "vat": 0, + "amount_incl": 0 + }, { "section": "Personnel", "description": "Opp + Drywers", @@ -580,16 +613,16 @@ "unit_price": 0, "amount_excl": 0, "vat": 0, - "amount_incl": 235000.0 + "amount_incl": 235000 }, { "section": "Subscriptions", "description": "Abrie - CCS", "gl_account": "CCS", "supplier": "AEB", - "qty": 1.0, - "unit_price": 2150.0, - "amount_excl": 2150.0, + "qty": 1, + "unit_price": 2150, + "amount_excl": 2150, "vat": 322.5, "amount_incl": 2472.5 }, @@ -598,9 +631,9 @@ "description": "Charl - CCS", "gl_account": "CCS", "supplier": "RIB", - "qty": 1.0, - "unit_price": 1970.0, - "amount_excl": 1970.0, + "qty": 1, + "unit_price": 1970, + "amount_excl": 1970, "vat": 2265.5, "amount_incl": 2265.5 }, @@ -609,7 +642,7 @@ "description": "Buildsmart Yearly update", "gl_account": "Buildsmart", "supplier": "Mega infra", - "qty": 1.0, + "qty": 1, "unit_price": 14827.91, "amount_excl": 14827.91, "vat": 2224.1865, @@ -622,9 +655,9 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 21250.0, - "vat": 3750.0, - "amount_incl": 25000.0 + "amount_excl": 21250, + "vat": 3750, + "amount_incl": 25000 }, { "section": "Rent & Utilities", @@ -633,8 +666,8 @@ "supplier": "", "qty": 0, "unit_price": 0, - "amount_excl": 3230.0, - "vat": 570.0, - "amount_incl": 3800.0 + "amount_excl": 3230, + "vat": 570, + "amount_incl": 3800 } ] \ No newline at end of file diff --git a/lib/data.ts b/lib/data.ts index d6fce6f..3829c47 100644 --- a/lib/data.ts +++ b/lib/data.ts @@ -6,11 +6,22 @@ export type Asset = { code: string; name: string; category: string; + type: string; gl_account: string; supplier: string; monthly_excl: number; monthly_incl: number; purchase_value: number; + is_provisional: boolean; +} & RecordModel; + +export type CostLine = { + id: string; + asset: string; + year: number; + month: number; + kind: "instalment" | "insurance" | "tracking" | "operator" | "running"; + amount: number; } & RecordModel; export type MonthlyIncome = { diff --git a/next.config.ts b/next.config.ts index 038c33d..744a662 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,6 +7,11 @@ const nextConfig: NextConfig = { const withPWA = withPWAInit({ dest: "public", + // Off in dev. next-pwa writes sw.js + workbox-*.js into `dest` on every + // compile, and the dev server watches public/ — so leaving it on makes each + // build trigger the next one, forever. A dev service worker also serves + // stale assets against HMR. Production builds are unaffected. + disable: process.env.NODE_ENV === "development", cacheOnFrontEndNav: true, aggressiveFrontEndNavCaching: true, reloadOnOnline: true, diff --git a/public/sw.js b/public/sw.js deleted file mode 100644 index 78bbd2a..0000000 --- a/public/sw.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright 2018 Google Inc. All Rights Reserved. - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * http://www.apache.org/licenses/LICENSE-2.0 - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// If the loader is already loaded, just stop. -if (!self.define) { - let registry = {}; - - // Used for `eval` and `importScripts` where we can't get script URL by other means. - // In both cases, it's safe to use a global var because those functions are synchronous. - let nextDefineUri; - - const singleRequire = (uri, parentUri) => { - uri = new URL(uri + ".js", parentUri).href; - return registry[uri] || ( - - new Promise(resolve => { - if ("document" in self) { - const script = document.createElement("script"); - script.src = uri; - script.onload = resolve; - document.head.appendChild(script); - } else { - nextDefineUri = uri; - importScripts(uri); - resolve(); - } - }) - - .then(() => { - let promise = registry[uri]; - if (!promise) { - throw new Error(`Module ${uri} didn’t register its module`); - } - return promise; - }) - ); - }; - - self.define = (depsNames, factory) => { - const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href; - if (registry[uri]) { - // Module is already loading or loaded. - return; - } - let exports = {}; - const require = depUri => singleRequire(depUri, uri); - const specialDeps = { - module: { uri }, - exports, - require - }; - registry[uri] = Promise.all(depsNames.map( - depName => specialDeps[depName] || require(depName) - )).then(deps => { - factory(...deps); - return exports; - }); - }; -} -define(['./workbox-7144475a'], (function (workbox) { 'use strict'; - - importScripts(); - self.skipWaiting(); - workbox.clientsClaim(); - workbox.registerRoute("/", new workbox.NetworkFirst({ - "cacheName": "start-url", - plugins: [{ - cacheWillUpdate: async ({ - response: e - }) => e && "opaqueredirect" === e.type ? new Response(e.body, { - status: 200, - statusText: "OK", - headers: e.headers - }) : e - }] - }), 'GET'); - workbox.registerRoute(/.*/i, new workbox.NetworkOnly({ - "cacheName": "dev", - plugins: [] - }), 'GET'); - -})); diff --git a/public/swe-worker-development.js b/public/swe-worker-development.js deleted file mode 100644 index fbea6b0..0000000 --- a/public/swe-worker-development.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - * ATTENTION: An "eval-source-map" devtool has been used. - * This devtool is neither made for production nor for readable output files. - * It uses "eval()" calls to create a separate source file with attached SourceMaps in the browser devtools. - * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) - * or disable the default devtool with "devtool: false". - * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). - */ -/******/ (() => { // webpackBootstrap -/******/ "use strict"; -/******/ var __webpack_modules__ = ({ - -/***/ "./node_modules/@ducanh2912/next-pwa/dist/sw-entry-worker.js": -/*!*******************************************************************!*\ - !*** ./node_modules/@ducanh2912/next-pwa/dist/sw-entry-worker.js ***! - \*******************************************************************/ -/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { - -eval(__webpack_require__.ts("__webpack_require__.r(__webpack_exports__);\nself.onmessage = async (e)=>{\n switch(e.data.type){\n case \"__START_URL_CACHE__\":\n {\n let t = e.data.url, a = await fetch(t);\n if (!a.redirected) return (await caches.open(\"start-url\")).put(t, a);\n return Promise.resolve();\n }\n case \"__FRONTEND_NAV_CACHE__\":\n {\n let t = e.data.url, a = await caches.open(\"pages\");\n if (await a.match(t, {\n ignoreSearch: !0\n })) return;\n let s = await fetch(t);\n if (!s.ok) return;\n if (a.put(t, s.clone()), e.data.shouldCacheAggressively && s.headers.get(\"Content-Type\")?.includes(\"text/html\")) try {\n let e = await s.text(), t = [], a = await caches.open(\"static-style-assets\"), r = await caches.open(\"next-static-js-assets\"), c = await caches.open(\"static-js-assets\");\n for (let [s, r] of e.matchAll(//g))/rel=['\"]stylesheet['\"]/.test(s) && t.push(a.match(r).then((e)=>e ? Promise.resolve() : a.add(r)));\n for (let [, a] of e.matchAll(//g)){\n let e = /\\/_next\\/static.+\\.js$/i.test(a) ? r : c;\n t.push(e.match(a).then((t)=>t ? Promise.resolve() : e.add(a)));\n }\n return await Promise.all(t);\n } catch {}\n return Promise.resolve();\n }\n default:\n return Promise.resolve();\n }\n};//# sourceURL=[module]\n//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9ub2RlX21vZHVsZXMvQGR1Y2FuaDI5MTIvbmV4dC1wd2EvZGlzdC9zdy1lbnRyeS13b3JrZXIuanMiLCJtYXBwaW5ncyI6IjtBQUFBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsaUJBQWlCO0FBQ2pCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0Esa0JBQWtCO0FBQ2xCO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQSIsInNvdXJjZXMiOlsiL1ZvbHVtZXMvRVhUL0RFVkVMT1BNRU5UL1BMQU5UX1RFU1Qvbm9kZV9tb2R1bGVzL0BkdWNhbmgyOTEyL25leHQtcHdhL2Rpc3Qvc3ctZW50cnktd29ya2VyLmpzIl0sInNvdXJjZXNDb250ZW50IjpbInNlbGYub25tZXNzYWdlID0gYXN5bmMgKGUpPT57XG4gICAgc3dpdGNoKGUuZGF0YS50eXBlKXtcbiAgICAgICAgY2FzZSBcIl9fU1RBUlRfVVJMX0NBQ0hFX19cIjpcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICBsZXQgdCA9IGUuZGF0YS51cmwsIGEgPSBhd2FpdCBmZXRjaCh0KTtcbiAgICAgICAgICAgICAgICBpZiAoIWEucmVkaXJlY3RlZCkgcmV0dXJuIChhd2FpdCBjYWNoZXMub3BlbihcInN0YXJ0LXVybFwiKSkucHV0KHQsIGEpO1xuICAgICAgICAgICAgICAgIHJldHVybiBQcm9taXNlLnJlc29sdmUoKTtcbiAgICAgICAgICAgIH1cbiAgICAgICAgY2FzZSBcIl9fRlJPTlRFTkRfTkFWX0NBQ0hFX19cIjpcbiAgICAgICAgICAgIHtcbiAgICAgICAgICAgICAgICBsZXQgdCA9IGUuZGF0YS51cmwsIGEgPSBhd2FpdCBjYWNoZXMub3BlbihcInBhZ2VzXCIpO1xuICAgICAgICAgICAgICAgIGlmIChhd2FpdCBhLm1hdGNoKHQsIHtcbiAgICAgICAgICAgICAgICAgICAgaWdub3JlU2VhcmNoOiAhMFxuICAgICAgICAgICAgICAgIH0pKSByZXR1cm47XG4gICAgICAgICAgICAgICAgbGV0IHMgPSBhd2FpdCBmZXRjaCh0KTtcbiAgICAgICAgICAgICAgICBpZiAoIXMub2spIHJldHVybjtcbiAgICAgICAgICAgICAgICBpZiAoYS5wdXQodCwgcy5jbG9uZSgpKSwgZS5kYXRhLnNob3VsZENhY2hlQWdncmVzc2l2ZWx5ICYmIHMuaGVhZGVycy5nZXQoXCJDb250ZW50LVR5cGVcIik/LmluY2x1ZGVzKFwidGV4dC9odG1sXCIpKSB0cnkge1xuICAgICAgICAgICAgICAgICAgICBsZXQgZSA9IGF3YWl0IHMudGV4dCgpLCB0ID0gW10sIGEgPSBhd2FpdCBjYWNoZXMub3BlbihcInN0YXRpYy1zdHlsZS1hc3NldHNcIiksIHIgPSBhd2FpdCBjYWNoZXMub3BlbihcIm5leHQtc3RhdGljLWpzLWFzc2V0c1wiKSwgYyA9IGF3YWl0IGNhY2hlcy5vcGVuKFwic3RhdGljLWpzLWFzc2V0c1wiKTtcbiAgICAgICAgICAgICAgICAgICAgZm9yIChsZXQgW3MsIHJdIG9mIGUubWF0Y2hBbGwoLzxsaW5rLio/aHJlZj1bJ1wiXSguKj8pWydcIl0uKj8+L2cpKS9yZWw9WydcIl1zdHlsZXNoZWV0WydcIl0vLnRlc3QocykgJiYgdC5wdXNoKGEubWF0Y2gocikudGhlbigoZSk9PmUgPyBQcm9taXNlLnJlc29sdmUoKSA6IGEuYWRkKHIpKSk7XG4gICAgICAgICAgICAgICAgICAgIGZvciAobGV0IFssIGFdIG9mIGUubWF0Y2hBbGwoLzxzY3JpcHQuKj9zcmM9WydcIl0oLio/KVsnXCJdLio/Pi9nKSl7XG4gICAgICAgICAgICAgICAgICAgICAgICBsZXQgZSA9IC9cXC9fbmV4dFxcL3N0YXRpYy4rXFwuanMkL2kudGVzdChhKSA/IHIgOiBjO1xuICAgICAgICAgICAgICAgICAgICAgICAgdC5wdXNoKGUubWF0Y2goYSkudGhlbigodCk9PnQgPyBQcm9taXNlLnJlc29sdmUoKSA6IGUuYWRkKGEpKSk7XG4gICAgICAgICAgICAgICAgICAgIH1cbiAgICAgICAgICAgICAgICAgICAgcmV0dXJuIGF3YWl0IFByb21pc2UuYWxsKHQpO1xuICAgICAgICAgICAgICAgIH0gY2F0Y2ggIHt9XG4gICAgICAgICAgICAgICAgcmV0dXJuIFByb21pc2UucmVzb2x2ZSgpO1xuICAgICAgICAgICAgfVxuICAgICAgICBkZWZhdWx0OlxuICAgICAgICAgICAgcmV0dXJuIFByb21pc2UucmVzb2x2ZSgpO1xuICAgIH1cbn07Il0sIm5hbWVzIjpbXSwiaWdub3JlTGlzdCI6WzBdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./node_modules/@ducanh2912/next-pwa/dist/sw-entry-worker.js\n")); - -/***/ }) - -/******/ }); -/************************************************************************/ -/******/ // The require scope -/******/ var __webpack_require__ = {}; -/******/ -/************************************************************************/ -/******/ /* webpack/runtime/make namespace object */ -/******/ (() => { -/******/ // define __esModule on exports -/******/ __webpack_require__.r = (exports) => { -/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { -/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); -/******/ } -/******/ Object.defineProperty(exports, '__esModule', { value: true }); -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/trusted types policy */ -/******/ (() => { -/******/ var policy; -/******/ __webpack_require__.tt = () => { -/******/ // Create Trusted Type policy if Trusted Types are available and the policy doesn't exist yet. -/******/ if (policy === undefined) { -/******/ policy = { -/******/ createScript: (script) => (script) -/******/ }; -/******/ if (typeof trustedTypes !== "undefined" && trustedTypes.createPolicy) { -/******/ policy = trustedTypes.createPolicy("nextjs#bundler", policy); -/******/ } -/******/ } -/******/ return policy; -/******/ }; -/******/ })(); -/******/ -/******/ /* webpack/runtime/trusted types script */ -/******/ (() => { -/******/ __webpack_require__.ts = (script) => (__webpack_require__.tt().createScript(script)); -/******/ })(); -/******/ -/******/ /* webpack/runtime/react refresh */ -/******/ (() => { -/******/ if (__webpack_require__.i) { -/******/ __webpack_require__.i.push((options) => { -/******/ const originalFactory = options.factory; -/******/ options.factory = (moduleObject, moduleExports, webpackRequire) => { -/******/ if (!originalFactory) { -/******/ document.location.reload(); -/******/ return; -/******/ } -/******/ const hasRefresh = typeof self !== "undefined" && !!self.$RefreshInterceptModuleExecution$; -/******/ const cleanup = hasRefresh ? self.$RefreshInterceptModuleExecution$(moduleObject.id) : () => {}; -/******/ try { -/******/ originalFactory.call(this, moduleObject, moduleExports, webpackRequire); -/******/ } finally { -/******/ cleanup(); -/******/ } -/******/ } -/******/ }) -/******/ } -/******/ })(); -/******/ -/******/ /* webpack/runtime/compat */ -/******/ -/******/ -/******/ // noop fns to prevent runtime errors during initialization -/******/ if (typeof self !== "undefined") { -/******/ self.$RefreshReg$ = function () {}; -/******/ self.$RefreshSig$ = function () { -/******/ return function (type) { -/******/ return type; -/******/ }; -/******/ }; -/******/ } -/******/ -/************************************************************************/ -/******/ -/******/ // startup -/******/ // Load entry module and return exports -/******/ // This entry module can't be inlined because the eval-source-map devtool is used. -/******/ var __webpack_exports__ = {}; -/******/ __webpack_modules__["./node_modules/@ducanh2912/next-pwa/dist/sw-entry-worker.js"](0, __webpack_exports__, __webpack_require__); -/******/ -/******/ })() -; \ No newline at end of file diff --git a/public/workbox-7144475a.js b/public/workbox-7144475a.js deleted file mode 100644 index 85db1a7..0000000 --- a/public/workbox-7144475a.js +++ /dev/null @@ -1,2455 +0,0 @@ -define(['exports'], (function (exports) { 'use strict'; - - // @ts-ignore - try { - self['workbox:core:7.0.0'] && _(); - } catch (e) {} - - /* - Copyright 2019 Google LLC - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const logger = (() => { - // Don't overwrite this value if it's already set. - // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 - if (!('__WB_DISABLE_DEV_LOGS' in globalThis)) { - self.__WB_DISABLE_DEV_LOGS = false; - } - let inGroup = false; - const methodToColorMap = { - debug: `#7f8c8d`, - log: `#2ecc71`, - warn: `#f39c12`, - error: `#c0392b`, - groupCollapsed: `#3498db`, - groupEnd: null // No colored prefix on groupEnd - }; - const print = function (method, args) { - if (self.__WB_DISABLE_DEV_LOGS) { - return; - } - if (method === 'groupCollapsed') { - // Safari doesn't print all console.groupCollapsed() arguments: - // https://bugs.webkit.org/show_bug.cgi?id=182754 - if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { - console[method](...args); - return; - } - } - const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`]; - // When in a group, the workbox prefix is not displayed. - const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; - console[method](...logPrefix, ...args); - if (method === 'groupCollapsed') { - inGroup = true; - } - if (method === 'groupEnd') { - inGroup = false; - } - }; - // eslint-disable-next-line @typescript-eslint/ban-types - const api = {}; - const loggerMethods = Object.keys(methodToColorMap); - for (const key of loggerMethods) { - const method = key; - api[method] = (...args) => { - print(method, args); - }; - } - return api; - })(); - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const messages$1 = { - 'invalid-value': ({ - paramName, - validValueDescription, - value - }) => { - if (!paramName || !validValueDescription) { - throw new Error(`Unexpected input to 'invalid-value' error.`); - } - return `The '${paramName}' parameter was given a value with an ` + `unexpected value. ${validValueDescription} Received a value of ` + `${JSON.stringify(value)}.`; - }, - 'not-an-array': ({ - moduleName, - className, - funcName, - paramName - }) => { - if (!moduleName || !className || !funcName || !paramName) { - throw new Error(`Unexpected input to 'not-an-array' error.`); - } - return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`; - }, - 'incorrect-type': ({ - expectedType, - paramName, - moduleName, - className, - funcName - }) => { - if (!expectedType || !paramName || !moduleName || !funcName) { - throw new Error(`Unexpected input to 'incorrect-type' error.`); - } - const classNameStr = className ? `${className}.` : ''; - return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}` + `${funcName}()' must be of type ${expectedType}.`; - }, - 'incorrect-class': ({ - expectedClassName, - paramName, - moduleName, - className, - funcName, - isReturnValueProblem - }) => { - if (!expectedClassName || !moduleName || !funcName) { - throw new Error(`Unexpected input to 'incorrect-class' error.`); - } - const classNameStr = className ? `${className}.` : ''; - if (isReturnValueProblem) { - return `The return value from ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`; - } - return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`; - }, - 'missing-a-method': ({ - expectedMethod, - paramName, - moduleName, - className, - funcName - }) => { - if (!expectedMethod || !paramName || !moduleName || !className || !funcName) { - throw new Error(`Unexpected input to 'missing-a-method' error.`); - } - return `${moduleName}.${className}.${funcName}() expected the ` + `'${paramName}' parameter to expose a '${expectedMethod}' method.`; - }, - 'add-to-cache-list-unexpected-type': ({ - entry - }) => { - return `An unexpected entry was passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + `strings with one or more characters, objects with a url property or ` + `Request objects.`; - }, - 'add-to-cache-list-conflicting-entries': ({ - firstEntry, - secondEntry - }) => { - if (!firstEntry || !secondEntry) { - throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`); - } - return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${firstEntry} but different revision details. Workbox is ` + `unable to cache and version the asset correctly. Please remove one ` + `of the entries.`; - }, - 'plugin-error-request-will-fetch': ({ - thrownErrorMessage - }) => { - if (!thrownErrorMessage) { - throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`); - } - return `An error was thrown by a plugins 'requestWillFetch()' method. ` + `The thrown error message was: '${thrownErrorMessage}'.`; - }, - 'invalid-cache-name': ({ - cacheNameId, - value - }) => { - if (!cacheNameId) { - throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`); - } - return `You must provide a name containing at least one character for ` + `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` + `'${JSON.stringify(value)}'`; - }, - 'unregister-route-but-not-found-with-method': ({ - method - }) => { - if (!method) { - throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`); - } - return `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`; - }, - 'unregister-route-route-not-registered': () => { - return `The route you're trying to unregister was not previously ` + `registered.`; - }, - 'queue-replay-failed': ({ - name - }) => { - return `Replaying the background sync queue '${name}' failed.`; - }, - 'duplicate-queue-name': ({ - name - }) => { - return `The Queue name '${name}' is already being used. ` + `All instances of backgroundSync.Queue must be given unique names.`; - }, - 'expired-test-without-max-age': ({ - methodName, - paramName - }) => { - return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`; - }, - 'unsupported-route-type': ({ - moduleName, - className, - funcName, - paramName - }) => { - return `The supplied '${paramName}' parameter was an unsupported type. ` + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + `valid input types.`; - }, - 'not-array-of-class': ({ - value, - expectedClass, - moduleName, - className, - funcName, - paramName - }) => { - return `The supplied '${paramName}' parameter must be an array of ` + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + `Please check the call to ${moduleName}.${className}.${funcName}() ` + `to fix the issue.`; - }, - 'max-entries-or-age-required': ({ - moduleName, - className, - funcName - }) => { - return `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`; - }, - 'statuses-or-headers-required': ({ - moduleName, - className, - funcName - }) => { - return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`; - }, - 'invalid-string': ({ - moduleName, - funcName, - paramName - }) => { - if (!paramName || !moduleName || !funcName) { - throw new Error(`Unexpected input to 'invalid-string' error.`); - } - return `When using strings, the '${paramName}' parameter must start with ` + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + `Please see the docs for ${moduleName}.${funcName}() for ` + `more info.`; - }, - 'channel-name-required': () => { - return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`; - }, - 'invalid-responses-are-same-args': () => { - return `The arguments passed into responsesAreSame() appear to be ` + `invalid. Please ensure valid Responses are used.`; - }, - 'expire-custom-caches-only': () => { - return `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`; - }, - 'unit-must-be-bytes': ({ - normalizedRangeHeader - }) => { - if (!normalizedRangeHeader) { - throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`); - } - return `The 'unit' portion of the Range header must be set to 'bytes'. ` + `The Range header provided was "${normalizedRangeHeader}"`; - }, - 'single-range-only': ({ - normalizedRangeHeader - }) => { - if (!normalizedRangeHeader) { - throw new Error(`Unexpected input to 'single-range-only' error.`); - } - return `Multiple ranges are not supported. Please use a single start ` + `value, and optional end value. The Range header provided was ` + `"${normalizedRangeHeader}"`; - }, - 'invalid-range-values': ({ - normalizedRangeHeader - }) => { - if (!normalizedRangeHeader) { - throw new Error(`Unexpected input to 'invalid-range-values' error.`); - } - return `The Range header is missing both start and end values. At least ` + `one of those values is needed. The Range header provided was ` + `"${normalizedRangeHeader}"`; - }, - 'no-range-header': () => { - return `No Range header was found in the Request provided.`; - }, - 'range-not-satisfiable': ({ - size, - start, - end - }) => { - return `The start (${start}) and end (${end}) values in the Range are ` + `not satisfiable by the cached response, which is ${size} bytes.`; - }, - 'attempt-to-cache-non-get-request': ({ - url, - method - }) => { - return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`; - }, - 'cache-put-with-no-response': ({ - url - }) => { - return `There was an attempt to cache '${url}' but the response was not ` + `defined.`; - }, - 'no-response': ({ - url, - error - }) => { - let message = `The strategy could not generate a response for '${url}'.`; - if (error) { - message += ` The underlying error is ${error}.`; - } - return message; - }, - 'bad-precaching-response': ({ - url, - status - }) => { - return `The precaching request for '${url}' failed` + (status ? ` with an HTTP status of ${status}.` : `.`); - }, - 'non-precached-url': ({ - url - }) => { - return `createHandlerBoundToURL('${url}') was called, but that URL is ` + `not precached. Please pass in a URL that is precached instead.`; - }, - 'add-to-cache-list-conflicting-integrities': ({ - url - }) => { - return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${url} with different integrity values. Please remove one of them.`; - }, - 'missing-precache-entry': ({ - cacheName, - url - }) => { - return `Unable to find a precached response in ${cacheName} for ${url}.`; - }, - 'cross-origin-copy-response': ({ - origin - }) => { - return `workbox-core.copyResponse() can only be used with same-origin ` + `responses. It was passed a response with origin ${origin}.`; - }, - 'opaque-streams-source': ({ - type - }) => { - const message = `One of the workbox-streams sources resulted in an ` + `'${type}' response.`; - if (type === 'opaqueredirect') { - return `${message} Please do not use a navigation request that results ` + `in a redirect as a source.`; - } - return `${message} Please ensure your sources are CORS-enabled.`; - } - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const generatorFunction = (code, details = {}) => { - const message = messages$1[code]; - if (!message) { - throw new Error(`Unable to find message for code '${code}'.`); - } - return message(details); - }; - const messageGenerator = generatorFunction; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Workbox errors should be thrown with this class. - * This allows use to ensure the type easily in tests, - * helps developers identify errors from workbox - * easily and allows use to optimise error - * messages correctly. - * - * @private - */ - class WorkboxError extends Error { - /** - * - * @param {string} errorCode The error code that - * identifies this particular error. - * @param {Object=} details Any relevant arguments - * that will help developers identify issues should - * be added as a key on the context object. - */ - constructor(errorCode, details) { - const message = messageGenerator(errorCode, details); - super(message); - this.name = errorCode; - this.details = details; - } - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /* - * This method throws if the supplied value is not an array. - * The destructed values are required to produce a meaningful error for users. - * The destructed and restructured object is so it's clear what is - * needed. - */ - const isArray = (value, details) => { - if (!Array.isArray(value)) { - throw new WorkboxError('not-an-array', details); - } - }; - const hasMethod = (object, expectedMethod, details) => { - const type = typeof object[expectedMethod]; - if (type !== 'function') { - details['expectedMethod'] = expectedMethod; - throw new WorkboxError('missing-a-method', details); - } - }; - const isType = (object, expectedType, details) => { - if (typeof object !== expectedType) { - details['expectedType'] = expectedType; - throw new WorkboxError('incorrect-type', details); - } - }; - const isInstance = (object, - // Need the general type to do the check later. - // eslint-disable-next-line @typescript-eslint/ban-types - expectedClass, details) => { - if (!(object instanceof expectedClass)) { - details['expectedClassName'] = expectedClass.name; - throw new WorkboxError('incorrect-class', details); - } - }; - const isOneOf = (value, validValues, details) => { - if (!validValues.includes(value)) { - details['validValueDescription'] = `Valid values are ${JSON.stringify(validValues)}.`; - throw new WorkboxError('invalid-value', details); - } - }; - const isArrayOfClass = (value, - // Need general type to do check later. - expectedClass, - // eslint-disable-line - details) => { - const error = new WorkboxError('not-array-of-class', details); - if (!Array.isArray(value)) { - throw error; - } - for (const item of value) { - if (!(item instanceof expectedClass)) { - throw error; - } - } - }; - const finalAssertExports = { - hasMethod, - isArray, - isInstance, - isOneOf, - isType, - isArrayOfClass - }; - - // @ts-ignore - try { - self['workbox:routing:7.0.0'] && _(); - } catch (e) {} - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * The default HTTP method, 'GET', used when there's no specific method - * configured for a route. - * - * @type {string} - * - * @private - */ - const defaultMethod = 'GET'; - /** - * The list of valid HTTP methods associated with requests that could be routed. - * - * @type {Array} - * - * @private - */ - const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * @param {function()|Object} handler Either a function, or an object with a - * 'handle' method. - * @return {Object} An object with a handle method. - * - * @private - */ - const normalizeHandler = handler => { - if (handler && typeof handler === 'object') { - { - finalAssertExports.hasMethod(handler, 'handle', { - moduleName: 'workbox-routing', - className: 'Route', - funcName: 'constructor', - paramName: 'handler' - }); - } - return handler; - } else { - { - finalAssertExports.isType(handler, 'function', { - moduleName: 'workbox-routing', - className: 'Route', - funcName: 'constructor', - paramName: 'handler' - }); - } - return { - handle: handler - }; - } - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * A `Route` consists of a pair of callback functions, "match" and "handler". - * The "match" callback determine if a route should be used to "handle" a - * request by returning a non-falsy value if it can. The "handler" callback - * is called when there is a match and should return a Promise that resolves - * to a `Response`. - * - * @memberof workbox-routing - */ - class Route { - /** - * Constructor for Route class. - * - * @param {workbox-routing~matchCallback} match - * A callback function that determines whether the route matches a given - * `fetch` event by returning a non-falsy value. - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resolving to a Response. - * @param {string} [method='GET'] The HTTP method to match the Route - * against. - */ - constructor(match, handler, method = defaultMethod) { - { - finalAssertExports.isType(match, 'function', { - moduleName: 'workbox-routing', - className: 'Route', - funcName: 'constructor', - paramName: 'match' - }); - if (method) { - finalAssertExports.isOneOf(method, validMethods, { - paramName: 'method' - }); - } - } - // These values are referenced directly by Router so cannot be - // altered by minificaton. - this.handler = normalizeHandler(handler); - this.match = match; - this.method = method; - } - /** - * - * @param {workbox-routing-handlerCallback} handler A callback - * function that returns a Promise resolving to a Response - */ - setCatchHandler(handler) { - this.catchHandler = normalizeHandler(handler); - } - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * RegExpRoute makes it easy to create a regular expression based - * {@link workbox-routing.Route}. - * - * For same-origin requests the RegExp only needs to match part of the URL. For - * requests against third-party servers, you must define a RegExp that matches - * the start of the URL. - * - * @memberof workbox-routing - * @extends workbox-routing.Route - */ - class RegExpRoute extends Route { - /** - * If the regular expression contains - * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references}, - * the captured values will be passed to the - * {@link workbox-routing~handlerCallback} `params` - * argument. - * - * @param {RegExp} regExp The regular expression to match against URLs. - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resulting in a Response. - * @param {string} [method='GET'] The HTTP method to match the Route - * against. - */ - constructor(regExp, handler, method) { - { - finalAssertExports.isInstance(regExp, RegExp, { - moduleName: 'workbox-routing', - className: 'RegExpRoute', - funcName: 'constructor', - paramName: 'pattern' - }); - } - const match = ({ - url - }) => { - const result = regExp.exec(url.href); - // Return immediately if there's no match. - if (!result) { - return; - } - // Require that the match start at the first character in the URL string - // if it's a cross-origin request. - // See https://github.com/GoogleChrome/workbox/issues/281 for the context - // behind this behavior. - if (url.origin !== location.origin && result.index !== 0) { - { - logger.debug(`The regular expression '${regExp.toString()}' only partially matched ` + `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`); - } - return; - } - // If the route matches, but there aren't any capture groups defined, then - // this will return [], which is truthy and therefore sufficient to - // indicate a match. - // If there are capture groups, then it will return their values. - return result.slice(1); - }; - super(match, handler, method); - } - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const getFriendlyURL = url => { - const urlObj = new URL(String(url), location.href); - // See https://github.com/GoogleChrome/workbox/issues/2323 - // We want to include everything, except for the origin if it's same-origin. - return urlObj.href.replace(new RegExp(`^${location.origin}`), ''); - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * The Router can be used to process a `FetchEvent` using one or more - * {@link workbox-routing.Route}, responding with a `Response` if - * a matching route exists. - * - * If no route matches a given a request, the Router will use a "default" - * handler if one is defined. - * - * Should the matching Route throw an error, the Router will use a "catch" - * handler if one is defined to gracefully deal with issues and respond with a - * Request. - * - * If a request matches multiple routes, the **earliest** registered route will - * be used to respond to the request. - * - * @memberof workbox-routing - */ - class Router { - /** - * Initializes a new Router. - */ - constructor() { - this._routes = new Map(); - this._defaultHandlerMap = new Map(); - } - /** - * @return {Map>} routes A `Map` of HTTP - * method name ('GET', etc.) to an array of all the corresponding `Route` - * instances that are registered. - */ - get routes() { - return this._routes; - } - /** - * Adds a fetch event listener to respond to events when a route matches - * the event's request. - */ - addFetchListener() { - // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 - self.addEventListener('fetch', event => { - const { - request - } = event; - const responsePromise = this.handleRequest({ - request, - event - }); - if (responsePromise) { - event.respondWith(responsePromise); - } - }); - } - /** - * Adds a message event listener for URLs to cache from the window. - * This is useful to cache resources loaded on the page prior to when the - * service worker started controlling it. - * - * The format of the message data sent from the window should be as follows. - * Where the `urlsToCache` array may consist of URL strings or an array of - * URL string + `requestInit` object (the same as you'd pass to `fetch()`). - * - * ``` - * { - * type: 'CACHE_URLS', - * payload: { - * urlsToCache: [ - * './script1.js', - * './script2.js', - * ['./script3.js', {mode: 'no-cors'}], - * ], - * }, - * } - * ``` - */ - addCacheListener() { - // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 - self.addEventListener('message', event => { - // event.data is type 'any' - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (event.data && event.data.type === 'CACHE_URLS') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const { - payload - } = event.data; - { - logger.debug(`Caching URLs from the window`, payload.urlsToCache); - } - const requestPromises = Promise.all(payload.urlsToCache.map(entry => { - if (typeof entry === 'string') { - entry = [entry]; - } - const request = new Request(...entry); - return this.handleRequest({ - request, - event - }); - // TODO(philipwalton): TypeScript errors without this typecast for - // some reason (probably a bug). The real type here should work but - // doesn't: `Array | undefined>`. - })); // TypeScript - event.waitUntil(requestPromises); - // If a MessageChannel was used, reply to the message on success. - if (event.ports && event.ports[0]) { - void requestPromises.then(() => event.ports[0].postMessage(true)); - } - } - }); - } - /** - * Apply the routing rules to a FetchEvent object to get a Response from an - * appropriate Route's handler. - * - * @param {Object} options - * @param {Request} options.request The request to handle. - * @param {ExtendableEvent} options.event The event that triggered the - * request. - * @return {Promise|undefined} A promise is returned if a - * registered route can handle the request. If there is no matching - * route and there's no `defaultHandler`, `undefined` is returned. - */ - handleRequest({ - request, - event - }) { - { - finalAssertExports.isInstance(request, Request, { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'handleRequest', - paramName: 'options.request' - }); - } - const url = new URL(request.url, location.href); - if (!url.protocol.startsWith('http')) { - { - logger.debug(`Workbox Router only supports URLs that start with 'http'.`); - } - return; - } - const sameOrigin = url.origin === location.origin; - const { - params, - route - } = this.findMatchingRoute({ - event, - request, - sameOrigin, - url - }); - let handler = route && route.handler; - const debugMessages = []; - { - if (handler) { - debugMessages.push([`Found a route to handle this request:`, route]); - if (params) { - debugMessages.push([`Passing the following params to the route's handler:`, params]); - } - } - } - // If we don't have a handler because there was no matching route, then - // fall back to defaultHandler if that's defined. - const method = request.method; - if (!handler && this._defaultHandlerMap.has(method)) { - { - debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler for ${method}.`); - } - handler = this._defaultHandlerMap.get(method); - } - if (!handler) { - { - // No handler so Workbox will do nothing. If logs is set of debug - // i.e. verbose, we should print out this information. - logger.debug(`No route found for: ${getFriendlyURL(url)}`); - } - return; - } - { - // We have a handler, meaning Workbox is going to handle the route. - // print the routing details to the console. - logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`); - debugMessages.forEach(msg => { - if (Array.isArray(msg)) { - logger.log(...msg); - } else { - logger.log(msg); - } - }); - logger.groupEnd(); - } - // Wrap in try and catch in case the handle method throws a synchronous - // error. It should still callback to the catch handler. - let responsePromise; - try { - responsePromise = handler.handle({ - url, - request, - event, - params - }); - } catch (err) { - responsePromise = Promise.reject(err); - } - // Get route's catch handler, if it exists - const catchHandler = route && route.catchHandler; - if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) { - responsePromise = responsePromise.catch(async err => { - // If there's a route catch handler, process that first - if (catchHandler) { - { - // Still include URL here as it will be async from the console group - // and may not make sense without the URL - logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`); - logger.error(`Error thrown by:`, route); - logger.error(err); - logger.groupEnd(); - } - try { - return await catchHandler.handle({ - url, - request, - event, - params - }); - } catch (catchErr) { - if (catchErr instanceof Error) { - err = catchErr; - } - } - } - if (this._catchHandler) { - { - // Still include URL here as it will be async from the console group - // and may not make sense without the URL - logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.`); - logger.error(`Error thrown by:`, route); - logger.error(err); - logger.groupEnd(); - } - return this._catchHandler.handle({ - url, - request, - event - }); - } - throw err; - }); - } - return responsePromise; - } - /** - * Checks a request and URL (and optionally an event) against the list of - * registered routes, and if there's a match, returns the corresponding - * route along with any params generated by the match. - * - * @param {Object} options - * @param {URL} options.url - * @param {boolean} options.sameOrigin The result of comparing `url.origin` - * against the current origin. - * @param {Request} options.request The request to match. - * @param {Event} options.event The corresponding event. - * @return {Object} An object with `route` and `params` properties. - * They are populated if a matching route was found or `undefined` - * otherwise. - */ - findMatchingRoute({ - url, - sameOrigin, - request, - event - }) { - const routes = this._routes.get(request.method) || []; - for (const route of routes) { - let params; - // route.match returns type any, not possible to change right now. - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const matchResult = route.match({ - url, - sameOrigin, - request, - event - }); - if (matchResult) { - { - // Warn developers that using an async matchCallback is almost always - // not the right thing to do. - if (matchResult instanceof Promise) { - logger.warn(`While routing ${getFriendlyURL(url)}, an async ` + `matchCallback function was used. Please convert the ` + `following route to use a synchronous matchCallback function:`, route); - } - } - // See https://github.com/GoogleChrome/workbox/issues/2079 - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - params = matchResult; - if (Array.isArray(params) && params.length === 0) { - // Instead of passing an empty array in as params, use undefined. - params = undefined; - } else if (matchResult.constructor === Object && - // eslint-disable-line - Object.keys(matchResult).length === 0) { - // Instead of passing an empty object in as params, use undefined. - params = undefined; - } else if (typeof matchResult === 'boolean') { - // For the boolean value true (rather than just something truth-y), - // don't set params. - // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353 - params = undefined; - } - // Return early if have a match. - return { - route, - params - }; - } - } - // If no match was found above, return and empty object. - return {}; - } - /** - * Define a default `handler` that's called when no routes explicitly - * match the incoming request. - * - * Each HTTP method ('GET', 'POST', etc.) gets its own default handler. - * - * Without a default handler, unmatched requests will go against the - * network as if there were no service worker present. - * - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resulting in a Response. - * @param {string} [method='GET'] The HTTP method to associate with this - * default handler. Each method has its own default. - */ - setDefaultHandler(handler, method = defaultMethod) { - this._defaultHandlerMap.set(method, normalizeHandler(handler)); - } - /** - * If a Route throws an error while handling a request, this `handler` - * will be called and given a chance to provide a response. - * - * @param {workbox-routing~handlerCallback} handler A callback - * function that returns a Promise resulting in a Response. - */ - setCatchHandler(handler) { - this._catchHandler = normalizeHandler(handler); - } - /** - * Registers a route with the router. - * - * @param {workbox-routing.Route} route The route to register. - */ - registerRoute(route) { - { - finalAssertExports.isType(route, 'object', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route' - }); - finalAssertExports.hasMethod(route, 'match', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route' - }); - finalAssertExports.isType(route.handler, 'object', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route' - }); - finalAssertExports.hasMethod(route.handler, 'handle', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route.handler' - }); - finalAssertExports.isType(route.method, 'string', { - moduleName: 'workbox-routing', - className: 'Router', - funcName: 'registerRoute', - paramName: 'route.method' - }); - } - if (!this._routes.has(route.method)) { - this._routes.set(route.method, []); - } - // Give precedence to all of the earlier routes by adding this additional - // route to the end of the array. - this._routes.get(route.method).push(route); - } - /** - * Unregisters a route with the router. - * - * @param {workbox-routing.Route} route The route to unregister. - */ - unregisterRoute(route) { - if (!this._routes.has(route.method)) { - throw new WorkboxError('unregister-route-but-not-found-with-method', { - method: route.method - }); - } - const routeIndex = this._routes.get(route.method).indexOf(route); - if (routeIndex > -1) { - this._routes.get(route.method).splice(routeIndex, 1); - } else { - throw new WorkboxError('unregister-route-route-not-registered'); - } - } - } - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - let defaultRouter; - /** - * Creates a new, singleton Router instance if one does not exist. If one - * does already exist, that instance is returned. - * - * @private - * @return {Router} - */ - const getOrCreateDefaultRouter = () => { - if (!defaultRouter) { - defaultRouter = new Router(); - // The helpers that use the default Router assume these listeners exist. - defaultRouter.addFetchListener(); - defaultRouter.addCacheListener(); - } - return defaultRouter; - }; - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Easily register a RegExp, string, or function with a caching - * strategy to a singleton Router instance. - * - * This method will generate a Route for you if needed and - * call {@link workbox-routing.Router#registerRoute}. - * - * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture - * If the capture param is a `Route`, all other arguments will be ignored. - * @param {workbox-routing~handlerCallback} [handler] A callback - * function that returns a Promise resulting in a Response. This parameter - * is required if `capture` is not a `Route` object. - * @param {string} [method='GET'] The HTTP method to match the Route - * against. - * @return {workbox-routing.Route} The generated `Route`. - * - * @memberof workbox-routing - */ - function registerRoute(capture, handler, method) { - let route; - if (typeof capture === 'string') { - const captureUrl = new URL(capture, location.href); - { - if (!(capture.startsWith('/') || capture.startsWith('http'))) { - throw new WorkboxError('invalid-string', { - moduleName: 'workbox-routing', - funcName: 'registerRoute', - paramName: 'capture' - }); - } - // We want to check if Express-style wildcards are in the pathname only. - // TODO: Remove this log message in v4. - const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture; - // See https://github.com/pillarjs/path-to-regexp#parameters - const wildcards = '[*:?+]'; - if (new RegExp(`${wildcards}`).exec(valueToCheck)) { - logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`); - } - } - const matchCallback = ({ - url - }) => { - { - if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { - logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url.toString()}. This route will only handle cross-origin requests ` + `if they match the entire URL.`); - } - } - return url.href === captureUrl.href; - }; - // If `capture` is a string then `handler` and `method` must be present. - route = new Route(matchCallback, handler, method); - } else if (capture instanceof RegExp) { - // If `capture` is a `RegExp` then `handler` and `method` must be present. - route = new RegExpRoute(capture, handler, method); - } else if (typeof capture === 'function') { - // If `capture` is a function then `handler` and `method` must be present. - route = new Route(capture, handler, method); - } else if (capture instanceof Route) { - route = capture; - } else { - throw new WorkboxError('unsupported-route-type', { - moduleName: 'workbox-routing', - funcName: 'registerRoute', - paramName: 'capture' - }); - } - const defaultRouter = getOrCreateDefaultRouter(); - defaultRouter.registerRoute(route); - return route; - } - - // @ts-ignore - try { - self['workbox:strategies:7.0.0'] && _(); - } catch (e) {} - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const cacheOkAndOpaquePlugin = { - /** - * Returns a valid response (to allow caching) if the status is 200 (OK) or - * 0 (opaque). - * - * @param {Object} options - * @param {Response} options.response - * @return {Response|null} - * - * @private - */ - cacheWillUpdate: async ({ - response - }) => { - if (response.status === 200 || response.status === 0) { - return response; - } - return null; - } - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const _cacheNameDetails = { - googleAnalytics: 'googleAnalytics', - precache: 'precache-v2', - prefix: 'workbox', - runtime: 'runtime', - suffix: typeof registration !== 'undefined' ? registration.scope : '' - }; - const _createCacheName = cacheName => { - return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix].filter(value => value && value.length > 0).join('-'); - }; - const eachCacheNameDetail = fn => { - for (const key of Object.keys(_cacheNameDetails)) { - fn(key); - } - }; - const cacheNames = { - updateDetails: details => { - eachCacheNameDetail(key => { - if (typeof details[key] === 'string') { - _cacheNameDetails[key] = details[key]; - } - }); - }, - getGoogleAnalyticsName: userCacheName => { - return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics); - }, - getPrecacheName: userCacheName => { - return userCacheName || _createCacheName(_cacheNameDetails.precache); - }, - getPrefix: () => { - return _cacheNameDetails.prefix; - }, - getRuntimeName: userCacheName => { - return userCacheName || _createCacheName(_cacheNameDetails.runtime); - }, - getSuffix: () => { - return _cacheNameDetails.suffix; - } - }; - - /* - Copyright 2020 Google LLC - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - function stripParams(fullURL, ignoreParams) { - const strippedURL = new URL(fullURL); - for (const param of ignoreParams) { - strippedURL.searchParams.delete(param); - } - return strippedURL.href; - } - /** - * Matches an item in the cache, ignoring specific URL params. This is similar - * to the `ignoreSearch` option, but it allows you to ignore just specific - * params (while continuing to match on the others). - * - * @private - * @param {Cache} cache - * @param {Request} request - * @param {Object} matchOptions - * @param {Array} ignoreParams - * @return {Promise} - */ - async function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) { - const strippedRequestURL = stripParams(request.url, ignoreParams); - // If the request doesn't include any ignored params, match as normal. - if (request.url === strippedRequestURL) { - return cache.match(request, matchOptions); - } - // Otherwise, match by comparing keys - const keysOptions = Object.assign(Object.assign({}, matchOptions), { - ignoreSearch: true - }); - const cacheKeys = await cache.keys(request, keysOptions); - for (const cacheKey of cacheKeys) { - const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams); - if (strippedRequestURL === strippedCacheKeyURL) { - return cache.match(cacheKey, matchOptions); - } - } - return; - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * The Deferred class composes Promises in a way that allows for them to be - * resolved or rejected from outside the constructor. In most cases promises - * should be used directly, but Deferreds can be necessary when the logic to - * resolve a promise must be separate. - * - * @private - */ - class Deferred { - /** - * Creates a promise and exposes its resolve and reject functions as methods. - */ - constructor() { - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - } - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - // Callbacks to be executed whenever there's a quota error. - // Can't change Function type right now. - // eslint-disable-next-line @typescript-eslint/ban-types - const quotaErrorCallbacks = new Set(); - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Runs all of the callback functions, one at a time sequentially, in the order - * in which they were registered. - * - * @memberof workbox-core - * @private - */ - async function executeQuotaErrorCallbacks() { - { - logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`); - } - for (const callback of quotaErrorCallbacks) { - await callback(); - { - logger.log(callback, 'is complete.'); - } - } - { - logger.log('Finished running callbacks.'); - } - } - - /* - Copyright 2019 Google LLC - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Returns a promise that resolves and the passed number of milliseconds. - * This utility is an async/await-friendly version of `setTimeout`. - * - * @param {number} ms - * @return {Promise} - * @private - */ - function timeout(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); - } - - /* - Copyright 2020 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - function toRequest(input) { - return typeof input === 'string' ? new Request(input) : input; - } - /** - * A class created every time a Strategy instance instance calls - * {@link workbox-strategies.Strategy~handle} or - * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and - * cache actions around plugin callbacks and keeps track of when the strategy - * is "done" (i.e. all added `event.waitUntil()` promises have resolved). - * - * @memberof workbox-strategies - */ - class StrategyHandler { - /** - * Creates a new instance associated with the passed strategy and event - * that's handling the request. - * - * The constructor also initializes the state that will be passed to each of - * the plugins handling this request. - * - * @param {workbox-strategies.Strategy} strategy - * @param {Object} options - * @param {Request|string} options.request A request to run this strategy for. - * @param {ExtendableEvent} options.event The event associated with the - * request. - * @param {URL} [options.url] - * @param {*} [options.params] The return value from the - * {@link workbox-routing~matchCallback} (if applicable). - */ - constructor(strategy, options) { - this._cacheKeys = {}; - /** - * The request the strategy is performing (passed to the strategy's - * `handle()` or `handleAll()` method). - * @name request - * @instance - * @type {Request} - * @memberof workbox-strategies.StrategyHandler - */ - /** - * The event associated with this request. - * @name event - * @instance - * @type {ExtendableEvent} - * @memberof workbox-strategies.StrategyHandler - */ - /** - * A `URL` instance of `request.url` (if passed to the strategy's - * `handle()` or `handleAll()` method). - * Note: the `url` param will be present if the strategy was invoked - * from a workbox `Route` object. - * @name url - * @instance - * @type {URL|undefined} - * @memberof workbox-strategies.StrategyHandler - */ - /** - * A `param` value (if passed to the strategy's - * `handle()` or `handleAll()` method). - * Note: the `param` param will be present if the strategy was invoked - * from a workbox `Route` object and the - * {@link workbox-routing~matchCallback} returned - * a truthy value (it will be that value). - * @name params - * @instance - * @type {*|undefined} - * @memberof workbox-strategies.StrategyHandler - */ - { - finalAssertExports.isInstance(options.event, ExtendableEvent, { - moduleName: 'workbox-strategies', - className: 'StrategyHandler', - funcName: 'constructor', - paramName: 'options.event' - }); - } - Object.assign(this, options); - this.event = options.event; - this._strategy = strategy; - this._handlerDeferred = new Deferred(); - this._extendLifetimePromises = []; - // Copy the plugins list (since it's mutable on the strategy), - // so any mutations don't affect this handler instance. - this._plugins = [...strategy.plugins]; - this._pluginStateMap = new Map(); - for (const plugin of this._plugins) { - this._pluginStateMap.set(plugin, {}); - } - this.event.waitUntil(this._handlerDeferred.promise); - } - /** - * Fetches a given request (and invokes any applicable plugin callback - * methods) using the `fetchOptions` (for non-navigation requests) and - * `plugins` defined on the `Strategy` object. - * - * The following plugin lifecycle methods are invoked when using this method: - * - `requestWillFetch()` - * - `fetchDidSucceed()` - * - `fetchDidFail()` - * - * @param {Request|string} input The URL or request to fetch. - * @return {Promise} - */ - async fetch(input) { - const { - event - } = this; - let request = toRequest(input); - if (request.mode === 'navigate' && event instanceof FetchEvent && event.preloadResponse) { - const possiblePreloadResponse = await event.preloadResponse; - if (possiblePreloadResponse) { - { - logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`); - } - return possiblePreloadResponse; - } - } - // If there is a fetchDidFail plugin, we need to save a clone of the - // original request before it's either modified by a requestWillFetch - // plugin or before the original request's body is consumed via fetch(). - const originalRequest = this.hasCallback('fetchDidFail') ? request.clone() : null; - try { - for (const cb of this.iterateCallbacks('requestWillFetch')) { - request = await cb({ - request: request.clone(), - event - }); - } - } catch (err) { - if (err instanceof Error) { - throw new WorkboxError('plugin-error-request-will-fetch', { - thrownErrorMessage: err.message - }); - } - } - // The request can be altered by plugins with `requestWillFetch` making - // the original request (most likely from a `fetch` event) different - // from the Request we make. Pass both to `fetchDidFail` to aid debugging. - const pluginFilteredRequest = request.clone(); - try { - let fetchResponse; - // See https://github.com/GoogleChrome/workbox/issues/1796 - fetchResponse = await fetch(request, request.mode === 'navigate' ? undefined : this._strategy.fetchOptions); - if ("development" !== 'production') { - logger.debug(`Network request for ` + `'${getFriendlyURL(request.url)}' returned a response with ` + `status '${fetchResponse.status}'.`); - } - for (const callback of this.iterateCallbacks('fetchDidSucceed')) { - fetchResponse = await callback({ - event, - request: pluginFilteredRequest, - response: fetchResponse - }); - } - return fetchResponse; - } catch (error) { - { - logger.log(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error); - } - // `originalRequest` will only exist if a `fetchDidFail` callback - // is being used (see above). - if (originalRequest) { - await this.runCallbacks('fetchDidFail', { - error: error, - event, - originalRequest: originalRequest.clone(), - request: pluginFilteredRequest.clone() - }); - } - throw error; - } - } - /** - * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on - * the response generated by `this.fetch()`. - * - * The call to `this.cachePut()` automatically invokes `this.waitUntil()`, - * so you do not have to manually call `waitUntil()` on the event. - * - * @param {Request|string} input The request or URL to fetch and cache. - * @return {Promise} - */ - async fetchAndCachePut(input) { - const response = await this.fetch(input); - const responseClone = response.clone(); - void this.waitUntil(this.cachePut(input, responseClone)); - return response; - } - /** - * Matches a request from the cache (and invokes any applicable plugin - * callback methods) using the `cacheName`, `matchOptions`, and `plugins` - * defined on the strategy object. - * - * The following plugin lifecycle methods are invoked when using this method: - * - cacheKeyWillByUsed() - * - cachedResponseWillByUsed() - * - * @param {Request|string} key The Request or URL to use as the cache key. - * @return {Promise} A matching response, if found. - */ - async cacheMatch(key) { - const request = toRequest(key); - let cachedResponse; - const { - cacheName, - matchOptions - } = this._strategy; - const effectiveRequest = await this.getCacheKey(request, 'read'); - const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), { - cacheName - }); - cachedResponse = await caches.match(effectiveRequest, multiMatchOptions); - { - if (cachedResponse) { - logger.debug(`Found a cached response in '${cacheName}'.`); - } else { - logger.debug(`No cached response found in '${cacheName}'.`); - } - } - for (const callback of this.iterateCallbacks('cachedResponseWillBeUsed')) { - cachedResponse = (await callback({ - cacheName, - matchOptions, - cachedResponse, - request: effectiveRequest, - event: this.event - })) || undefined; - } - return cachedResponse; - } - /** - * Puts a request/response pair in the cache (and invokes any applicable - * plugin callback methods) using the `cacheName` and `plugins` defined on - * the strategy object. - * - * The following plugin lifecycle methods are invoked when using this method: - * - cacheKeyWillByUsed() - * - cacheWillUpdate() - * - cacheDidUpdate() - * - * @param {Request|string} key The request or URL to use as the cache key. - * @param {Response} response The response to cache. - * @return {Promise} `false` if a cacheWillUpdate caused the response - * not be cached, and `true` otherwise. - */ - async cachePut(key, response) { - const request = toRequest(key); - // Run in the next task to avoid blocking other cache reads. - // https://github.com/w3c/ServiceWorker/issues/1397 - await timeout(0); - const effectiveRequest = await this.getCacheKey(request, 'write'); - { - if (effectiveRequest.method && effectiveRequest.method !== 'GET') { - throw new WorkboxError('attempt-to-cache-non-get-request', { - url: getFriendlyURL(effectiveRequest.url), - method: effectiveRequest.method - }); - } - // See https://github.com/GoogleChrome/workbox/issues/2818 - const vary = response.headers.get('Vary'); - if (vary) { - logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} ` + `has a 'Vary: ${vary}' header. ` + `Consider setting the {ignoreVary: true} option on your strategy ` + `to ensure cache matching and deletion works as expected.`); - } - } - if (!response) { - { - logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`); - } - throw new WorkboxError('cache-put-with-no-response', { - url: getFriendlyURL(effectiveRequest.url) - }); - } - const responseToCache = await this._ensureResponseSafeToCache(response); - if (!responseToCache) { - { - logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` + `will not be cached.`, responseToCache); - } - return false; - } - const { - cacheName, - matchOptions - } = this._strategy; - const cache = await self.caches.open(cacheName); - const hasCacheUpdateCallback = this.hasCallback('cacheDidUpdate'); - const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams( - // TODO(philipwalton): the `__WB_REVISION__` param is a precaching - // feature. Consider into ways to only add this behavior if using - // precaching. - cache, effectiveRequest.clone(), ['__WB_REVISION__'], matchOptions) : null; - { - logger.debug(`Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.`); - } - try { - await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache); - } catch (error) { - if (error instanceof Error) { - // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError - if (error.name === 'QuotaExceededError') { - await executeQuotaErrorCallbacks(); - } - throw error; - } - } - for (const callback of this.iterateCallbacks('cacheDidUpdate')) { - await callback({ - cacheName, - oldResponse, - newResponse: responseToCache.clone(), - request: effectiveRequest, - event: this.event - }); - } - return true; - } - /** - * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and - * executes any of those callbacks found in sequence. The final `Request` - * object returned by the last plugin is treated as the cache key for cache - * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have - * been registered, the passed request is returned unmodified - * - * @param {Request} request - * @param {string} mode - * @return {Promise} - */ - async getCacheKey(request, mode) { - const key = `${request.url} | ${mode}`; - if (!this._cacheKeys[key]) { - let effectiveRequest = request; - for (const callback of this.iterateCallbacks('cacheKeyWillBeUsed')) { - effectiveRequest = toRequest(await callback({ - mode, - request: effectiveRequest, - event: this.event, - // params has a type any can't change right now. - params: this.params // eslint-disable-line - })); - } - this._cacheKeys[key] = effectiveRequest; - } - return this._cacheKeys[key]; - } - /** - * Returns true if the strategy has at least one plugin with the given - * callback. - * - * @param {string} name The name of the callback to check for. - * @return {boolean} - */ - hasCallback(name) { - for (const plugin of this._strategy.plugins) { - if (name in plugin) { - return true; - } - } - return false; - } - /** - * Runs all plugin callbacks matching the given name, in order, passing the - * given param object (merged ith the current plugin state) as the only - * argument. - * - * Note: since this method runs all plugins, it's not suitable for cases - * where the return value of a callback needs to be applied prior to calling - * the next callback. See - * {@link workbox-strategies.StrategyHandler#iterateCallbacks} - * below for how to handle that case. - * - * @param {string} name The name of the callback to run within each plugin. - * @param {Object} param The object to pass as the first (and only) param - * when executing each callback. This object will be merged with the - * current plugin state prior to callback execution. - */ - async runCallbacks(name, param) { - for (const callback of this.iterateCallbacks(name)) { - // TODO(philipwalton): not sure why `any` is needed. It seems like - // this should work with `as WorkboxPluginCallbackParam[C]`. - await callback(param); - } - } - /** - * Accepts a callback and returns an iterable of matching plugin callbacks, - * where each callback is wrapped with the current handler state (i.e. when - * you call each callback, whatever object parameter you pass it will - * be merged with the plugin's current state). - * - * @param {string} name The name fo the callback to run - * @return {Array} - */ - *iterateCallbacks(name) { - for (const plugin of this._strategy.plugins) { - if (typeof plugin[name] === 'function') { - const state = this._pluginStateMap.get(plugin); - const statefulCallback = param => { - const statefulParam = Object.assign(Object.assign({}, param), { - state - }); - // TODO(philipwalton): not sure why `any` is needed. It seems like - // this should work with `as WorkboxPluginCallbackParam[C]`. - return plugin[name](statefulParam); - }; - yield statefulCallback; - } - } - } - /** - * Adds a promise to the - * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises} - * of the event event associated with the request being handled (usually a - * `FetchEvent`). - * - * Note: you can await - * {@link workbox-strategies.StrategyHandler~doneWaiting} - * to know when all added promises have settled. - * - * @param {Promise} promise A promise to add to the extend lifetime promises - * of the event that triggered the request. - */ - waitUntil(promise) { - this._extendLifetimePromises.push(promise); - return promise; - } - /** - * Returns a promise that resolves once all promises passed to - * {@link workbox-strategies.StrategyHandler~waitUntil} - * have settled. - * - * Note: any work done after `doneWaiting()` settles should be manually - * passed to an event's `waitUntil()` method (not this handler's - * `waitUntil()` method), otherwise the service worker thread my be killed - * prior to your work completing. - */ - async doneWaiting() { - let promise; - while (promise = this._extendLifetimePromises.shift()) { - await promise; - } - } - /** - * Stops running the strategy and immediately resolves any pending - * `waitUntil()` promises. - */ - destroy() { - this._handlerDeferred.resolve(null); - } - /** - * This method will call cacheWillUpdate on the available plugins (or use - * status === 200) to determine if the Response is safe and valid to cache. - * - * @param {Request} options.request - * @param {Response} options.response - * @return {Promise} - * - * @private - */ - async _ensureResponseSafeToCache(response) { - let responseToCache = response; - let pluginsUsed = false; - for (const callback of this.iterateCallbacks('cacheWillUpdate')) { - responseToCache = (await callback({ - request: this.request, - response: responseToCache, - event: this.event - })) || undefined; - pluginsUsed = true; - if (!responseToCache) { - break; - } - } - if (!pluginsUsed) { - if (responseToCache && responseToCache.status !== 200) { - responseToCache = undefined; - } - { - if (responseToCache) { - if (responseToCache.status !== 200) { - if (responseToCache.status === 0) { - logger.warn(`The response for '${this.request.url}' ` + `is an opaque response. The caching strategy that you're ` + `using will not cache opaque responses by default.`); - } else { - logger.debug(`The response for '${this.request.url}' ` + `returned a status code of '${response.status}' and won't ` + `be cached as a result.`); - } - } - } - } - } - return responseToCache; - } - } - - /* - Copyright 2020 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * An abstract base class that all other strategy classes must extend from: - * - * @memberof workbox-strategies - */ - class Strategy { - /** - * Creates a new instance of the strategy and sets all documented option - * properties as public instance properties. - * - * Note: if a custom strategy class extends the base Strategy class and does - * not need more than these properties, it does not need to define its own - * constructor. - * - * @param {Object} [options] - * @param {string} [options.cacheName] Cache name to store and retrieve - * requests. Defaults to the cache names provided by - * {@link workbox-core.cacheNames}. - * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} - * to use in conjunction with this caching strategy. - * @param {Object} [options.fetchOptions] Values passed along to the - * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) - * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796) - * `fetch()` requests made by this strategy. - * @param {Object} [options.matchOptions] The - * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} - * for any `cache.match()` or `cache.put()` calls made by this strategy. - */ - constructor(options = {}) { - /** - * Cache name to store and retrieve - * requests. Defaults to the cache names provided by - * {@link workbox-core.cacheNames}. - * - * @type {string} - */ - this.cacheName = cacheNames.getRuntimeName(options.cacheName); - /** - * The list - * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} - * used by this strategy. - * - * @type {Array} - */ - this.plugins = options.plugins || []; - /** - * Values passed along to the - * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters} - * of all fetch() requests made by this strategy. - * - * @type {Object} - */ - this.fetchOptions = options.fetchOptions; - /** - * The - * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} - * for any `cache.match()` or `cache.put()` calls made by this strategy. - * - * @type {Object} - */ - this.matchOptions = options.matchOptions; - } - /** - * Perform a request strategy and returns a `Promise` that will resolve with - * a `Response`, invoking all relevant plugin callbacks. - * - * When a strategy instance is registered with a Workbox - * {@link workbox-routing.Route}, this method is automatically - * called when the route matches. - * - * Alternatively, this method can be used in a standalone `FetchEvent` - * listener by passing it to `event.respondWith()`. - * - * @param {FetchEvent|Object} options A `FetchEvent` or an object with the - * properties listed below. - * @param {Request|string} options.request A request to run this strategy for. - * @param {ExtendableEvent} options.event The event associated with the - * request. - * @param {URL} [options.url] - * @param {*} [options.params] - */ - handle(options) { - const [responseDone] = this.handleAll(options); - return responseDone; - } - /** - * Similar to {@link workbox-strategies.Strategy~handle}, but - * instead of just returning a `Promise` that resolves to a `Response` it - * it will return an tuple of `[response, done]` promises, where the former - * (`response`) is equivalent to what `handle()` returns, and the latter is a - * Promise that will resolve once any promises that were added to - * `event.waitUntil()` as part of performing the strategy have completed. - * - * You can await the `done` promise to ensure any extra work performed by - * the strategy (usually caching responses) completes successfully. - * - * @param {FetchEvent|Object} options A `FetchEvent` or an object with the - * properties listed below. - * @param {Request|string} options.request A request to run this strategy for. - * @param {ExtendableEvent} options.event The event associated with the - * request. - * @param {URL} [options.url] - * @param {*} [options.params] - * @return {Array} A tuple of [response, done] - * promises that can be used to determine when the response resolves as - * well as when the handler has completed all its work. - */ - handleAll(options) { - // Allow for flexible options to be passed. - if (options instanceof FetchEvent) { - options = { - event: options, - request: options.request - }; - } - const event = options.event; - const request = typeof options.request === 'string' ? new Request(options.request) : options.request; - const params = 'params' in options ? options.params : undefined; - const handler = new StrategyHandler(this, { - event, - request, - params - }); - const responseDone = this._getResponse(handler, request, event); - const handlerDone = this._awaitComplete(responseDone, handler, request, event); - // Return an array of promises, suitable for use with Promise.all(). - return [responseDone, handlerDone]; - } - async _getResponse(handler, request, event) { - await handler.runCallbacks('handlerWillStart', { - event, - request - }); - let response = undefined; - try { - response = await this._handle(request, handler); - // The "official" Strategy subclasses all throw this error automatically, - // but in case a third-party Strategy doesn't, ensure that we have a - // consistent failure when there's no response or an error response. - if (!response || response.type === 'error') { - throw new WorkboxError('no-response', { - url: request.url - }); - } - } catch (error) { - if (error instanceof Error) { - for (const callback of handler.iterateCallbacks('handlerDidError')) { - response = await callback({ - error, - event, - request - }); - if (response) { - break; - } - } - } - if (!response) { - throw error; - } else { - logger.log(`While responding to '${getFriendlyURL(request.url)}', ` + `an ${error instanceof Error ? error.toString() : ''} error occurred. Using a fallback response provided by ` + `a handlerDidError plugin.`); - } - } - for (const callback of handler.iterateCallbacks('handlerWillRespond')) { - response = await callback({ - event, - request, - response - }); - } - return response; - } - async _awaitComplete(responseDone, handler, request, event) { - let response; - let error; - try { - response = await responseDone; - } catch (error) { - // Ignore errors, as response errors should be caught via the `response` - // promise above. The `done` promise will only throw for errors in - // promises passed to `handler.waitUntil()`. - } - try { - await handler.runCallbacks('handlerDidRespond', { - event, - request, - response - }); - await handler.doneWaiting(); - } catch (waitUntilError) { - if (waitUntilError instanceof Error) { - error = waitUntilError; - } - } - await handler.runCallbacks('handlerDidComplete', { - event, - request, - response, - error: error - }); - handler.destroy(); - if (error) { - throw error; - } - } - } - /** - * Classes extending the `Strategy` based class should implement this method, - * and leverage the {@link workbox-strategies.StrategyHandler} - * arg to perform all fetching and cache logic, which will ensure all relevant - * cache, cache options, fetch options and plugins are used (per the current - * strategy instance). - * - * @name _handle - * @instance - * @abstract - * @function - * @param {Request} request - * @param {workbox-strategies.StrategyHandler} handler - * @return {Promise} - * - * @memberof workbox-strategies.Strategy - */ - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - const messages = { - strategyStart: (strategyName, request) => `Using ${strategyName} to respond to '${getFriendlyURL(request.url)}'`, - printFinalResponse: response => { - if (response) { - logger.groupCollapsed(`View the final response here.`); - logger.log(response || '[No response returned]'); - logger.groupEnd(); - } - } - }; - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * An implementation of a - * [network first](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#network-first-falling-back-to-cache) - * request strategy. - * - * By default, this strategy will cache responses with a 200 status code as - * well as [opaque responses](https://developer.chrome.com/docs/workbox/caching-resources-during-runtime/#opaque-responses). - * Opaque responses are are cross-origin requests where the response doesn't - * support [CORS](https://enable-cors.org/). - * - * If the network request fails, and there is no cache match, this will throw - * a `WorkboxError` exception. - * - * @extends workbox-strategies.Strategy - * @memberof workbox-strategies - */ - class NetworkFirst extends Strategy { - /** - * @param {Object} [options] - * @param {string} [options.cacheName] Cache name to store and retrieve - * requests. Defaults to cache names provided by - * {@link workbox-core.cacheNames}. - * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} - * to use in conjunction with this caching strategy. - * @param {Object} [options.fetchOptions] Values passed along to the - * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) - * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796) - * `fetch()` requests made by this strategy. - * @param {Object} [options.matchOptions] [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) - * @param {number} [options.networkTimeoutSeconds] If set, any network requests - * that fail to respond within the timeout will fallback to the cache. - * - * This option can be used to combat - * "[lie-fi]{@link https://developers.google.com/web/fundamentals/performance/poor-connectivity/#lie-fi}" - * scenarios. - */ - constructor(options = {}) { - super(options); - // If this instance contains no plugins with a 'cacheWillUpdate' callback, - // prepend the `cacheOkAndOpaquePlugin` plugin to the plugins list. - if (!this.plugins.some(p => 'cacheWillUpdate' in p)) { - this.plugins.unshift(cacheOkAndOpaquePlugin); - } - this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0; - { - if (this._networkTimeoutSeconds) { - finalAssertExports.isType(this._networkTimeoutSeconds, 'number', { - moduleName: 'workbox-strategies', - className: this.constructor.name, - funcName: 'constructor', - paramName: 'networkTimeoutSeconds' - }); - } - } - } - /** - * @private - * @param {Request|string} request A request to run this strategy for. - * @param {workbox-strategies.StrategyHandler} handler The event that - * triggered the request. - * @return {Promise} - */ - async _handle(request, handler) { - const logs = []; - { - finalAssertExports.isInstance(request, Request, { - moduleName: 'workbox-strategies', - className: this.constructor.name, - funcName: 'handle', - paramName: 'makeRequest' - }); - } - const promises = []; - let timeoutId; - if (this._networkTimeoutSeconds) { - const { - id, - promise - } = this._getTimeoutPromise({ - request, - logs, - handler - }); - timeoutId = id; - promises.push(promise); - } - const networkPromise = this._getNetworkPromise({ - timeoutId, - request, - logs, - handler - }); - promises.push(networkPromise); - const response = await handler.waitUntil((async () => { - // Promise.race() will resolve as soon as the first promise resolves. - return (await handler.waitUntil(Promise.race(promises))) || ( - // If Promise.race() resolved with null, it might be due to a network - // timeout + a cache miss. If that were to happen, we'd rather wait until - // the networkPromise resolves instead of returning null. - // Note that it's fine to await an already-resolved promise, so we don't - // have to check to see if it's still "in flight". - await networkPromise); - })()); - { - logger.groupCollapsed(messages.strategyStart(this.constructor.name, request)); - for (const log of logs) { - logger.log(log); - } - messages.printFinalResponse(response); - logger.groupEnd(); - } - if (!response) { - throw new WorkboxError('no-response', { - url: request.url - }); - } - return response; - } - /** - * @param {Object} options - * @param {Request} options.request - * @param {Array} options.logs A reference to the logs array - * @param {Event} options.event - * @return {Promise} - * - * @private - */ - _getTimeoutPromise({ - request, - logs, - handler - }) { - let timeoutId; - const timeoutPromise = new Promise(resolve => { - const onNetworkTimeout = async () => { - { - logs.push(`Timing out the network response at ` + `${this._networkTimeoutSeconds} seconds.`); - } - resolve(await handler.cacheMatch(request)); - }; - timeoutId = setTimeout(onNetworkTimeout, this._networkTimeoutSeconds * 1000); - }); - return { - promise: timeoutPromise, - id: timeoutId - }; - } - /** - * @param {Object} options - * @param {number|undefined} options.timeoutId - * @param {Request} options.request - * @param {Array} options.logs A reference to the logs Array. - * @param {Event} options.event - * @return {Promise} - * - * @private - */ - async _getNetworkPromise({ - timeoutId, - request, - logs, - handler - }) { - let error; - let response; - try { - response = await handler.fetchAndCachePut(request); - } catch (fetchError) { - if (fetchError instanceof Error) { - error = fetchError; - } - } - if (timeoutId) { - clearTimeout(timeoutId); - } - { - if (response) { - logs.push(`Got response from network.`); - } else { - logs.push(`Unable to get a response from the network. Will respond ` + `with a cached response.`); - } - } - if (error || !response) { - response = await handler.cacheMatch(request); - { - if (response) { - logs.push(`Found a cached response in the '${this.cacheName}'` + ` cache.`); - } else { - logs.push(`No response found in the '${this.cacheName}' cache.`); - } - } - } - return response; - } - } - - /* - Copyright 2018 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * An implementation of a - * [network-only](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#network-only) - * request strategy. - * - * This class is useful if you want to take advantage of any - * [Workbox plugins](https://developer.chrome.com/docs/workbox/using-plugins/). - * - * If the network request fails, this will throw a `WorkboxError` exception. - * - * @extends workbox-strategies.Strategy - * @memberof workbox-strategies - */ - class NetworkOnly extends Strategy { - /** - * @param {Object} [options] - * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} - * to use in conjunction with this caching strategy. - * @param {Object} [options.fetchOptions] Values passed along to the - * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) - * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796) - * `fetch()` requests made by this strategy. - * @param {number} [options.networkTimeoutSeconds] If set, any network requests - * that fail to respond within the timeout will result in a network error. - */ - constructor(options = {}) { - super(options); - this._networkTimeoutSeconds = options.networkTimeoutSeconds || 0; - } - /** - * @private - * @param {Request|string} request A request to run this strategy for. - * @param {workbox-strategies.StrategyHandler} handler The event that - * triggered the request. - * @return {Promise} - */ - async _handle(request, handler) { - { - finalAssertExports.isInstance(request, Request, { - moduleName: 'workbox-strategies', - className: this.constructor.name, - funcName: '_handle', - paramName: 'request' - }); - } - let error = undefined; - let response; - try { - const promises = [handler.fetch(request)]; - if (this._networkTimeoutSeconds) { - const timeoutPromise = timeout(this._networkTimeoutSeconds * 1000); - promises.push(timeoutPromise); - } - response = await Promise.race(promises); - if (!response) { - throw new Error(`Timed out the network response after ` + `${this._networkTimeoutSeconds} seconds.`); - } - } catch (err) { - if (err instanceof Error) { - error = err; - } - } - { - logger.groupCollapsed(messages.strategyStart(this.constructor.name, request)); - if (response) { - logger.log(`Got response from network.`); - } else { - logger.log(`Unable to get a response from the network.`); - } - messages.printFinalResponse(response); - logger.groupEnd(); - } - if (!response) { - throw new WorkboxError('no-response', { - url: request.url, - error - }); - } - return response; - } - } - - /* - Copyright 2019 Google LLC - - Use of this source code is governed by an MIT-style - license that can be found in the LICENSE file or at - https://opensource.org/licenses/MIT. - */ - /** - * Claim any currently available clients once the service worker - * becomes active. This is normally used in conjunction with `skipWaiting()`. - * - * @memberof workbox-core - */ - function clientsClaim() { - self.addEventListener('activate', () => self.clients.claim()); - } - - exports.NetworkFirst = NetworkFirst; - exports.NetworkOnly = NetworkOnly; - exports.clientsClaim = clientsClaim; - exports.registerRoute = registerRoute; - -})); diff --git a/scripts/extract.mjs b/scripts/extract.mjs index d65bf6b..8e330f3 100644 --- a/scripts/extract.mjs +++ b/scripts/extract.mjs @@ -3,7 +3,7 @@ // Writes: ./data/source/{assets,machines,mrc,overheads}.json // // Usage: node scripts/extract.mjs -import * as XLSX from "xlsx"; +import XLSX from "xlsx"; import { writeFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, resolve } from "node:path"; @@ -97,6 +97,35 @@ console.log(` ✓ ${assets.length} assets`); console.log("\n→ Extracting machines from Machine Graph Data"); const wsMGD = wb.Sheets["Machine Graph Data"]; const monthNames = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; + +// Part C.3: assign sequential codes for uncoded duplicates in the workbook. +// Maps "TYPE_" to a counter so "SITE CONTANER #1" → SC01, etc. +// We also fix the spelling (CONTANER → CONTAINER) so it matches assets.json. +// Each entry: { prefix, test(name) → bool, normalised(name) → string } +const UNCODED_TYPES = [ + { prefix: "SC", test: (n) => /^SITE CONTANER/i.test(n) || /^Site Container/i.test(n) }, + { prefix: "OC", test: (n) => /^Office Container/i.test(n) }, + { prefix: "PC", test: (n) => /^Porta camp/i.test(n) }, + { prefix: "RV02", test: (n) => /^ELB 1TON RV/i.test(n) }, + { prefix: "GN100", test: (n) => /^10 KVA GENERATOR/i.test(n) }, + { prefix: "GN075", test: (n) => /^7,5 KVA GENERATOR/i.test(n) }, + { prefix: "GN300", test: (n) => /^30KVA GENERATOR/i.test(n) }, + { prefix: "TR01", test: (n) => /^TRAILOR PLANT/i.test(n) }, + { prefix: "TR02", test: (n) => /^DIESEL TRAILOR/i.test(n) }, + { prefix: "SO01", test: (n) => /^Solar office/i.test(n) }, +]; +const uncodedCounters = {}; +function codeForUncoded(rawName) { + // Normalise the workbook spelling to match assets.json + const name = rawName.replace(/^SITE CONTANER/, "Site Container").trim(); + const match = UNCODED_TYPES.find((t) => t.test(name)); + if (!match) return null; + // Fixed code (single instance) vs sequential + if (/^\D+\d+$/.test(match.prefix)) return match.prefix; + uncodedCounters[match.prefix] = (uncodedCounters[match.prefix] || 0) + 1; + return `${match.prefix}${String(uncodedCounters[match.prefix]).padStart(2, "0")}`; +} + const machineRows = []; let currentMachine = null; let currentData = []; @@ -134,17 +163,26 @@ for (let r = range.s.r; r <= range.e.r; r++) { } if (currentMachine && currentData.length) machineRows.push({ machine: currentMachine, data: currentData }); -// Strip code from "NAME (CODE)" pattern +// Strip code from "NAME (CODE)" pattern OR assign sequential code for uncoded types const machines = machineRows.map((m) => { const match = m.machine.match(/\(([^)]+)\)\s*$/); + if (match) { + return { + machine: m.machine.replace(/\s*\([^)]+\)\s*$/, "").trim(), + code: match[1].trim(), + data: m.data, + }; + } + // Try sequential-code assignment (Part C.3) + const syntheticCode = codeForUncoded(m.machine); return { - machine: m.machine.replace(/\s*\([^)]+\)\s*$/, "").trim(), - code: match ? match[1].trim() : "", + machine: m.machine.replace(/^SITE CONTANER/, "Site Container").trim(), + code: syntheticCode || "", data: m.data, }; }); writeFileSync(resolve(__dirname, "..", "data", "source", "machines.json"), JSON.stringify(machines, null, 2)); -console.log(` ✓ ${machines.length} machines`); +console.log(` ✓ ${machines.length} machines (sequential codes: ${Object.keys(uncodedCounters).length} prefixes)`); // === MRC from Monthly Running Cost sheet === console.log("\n→ Extracting monthly running costs"); diff --git a/scripts/seed.mjs b/scripts/seed.mjs index 6dfcf19..df3852e 100644 --- a/scripts/seed.mjs +++ b/scripts/seed.mjs @@ -28,11 +28,13 @@ const ASSETS_COLL = "assets"; const INCOME_COLL = "monthly_income"; const RUN_COLL = "monthly_running_costs"; const OVERHEADS_COLL = "overheads"; +const COST_LINES_COLL = "cost_lines"; const assets = JSON.parse(readFileSync(resolve(DATA_DIR, "assets.json"), "utf8")); const machines = JSON.parse(readFileSync(resolve(DATA_DIR, "machines.json"), "utf8")); const mrc = JSON.parse(readFileSync(resolve(DATA_DIR, "mrc.json"), "utf8")); const overheads = JSON.parse(readFileSync(resolve(DATA_DIR, "overheads.json"), "utf8")); +const costLines = JSON.parse(readFileSync(resolve(DATA_DIR, "cost_lines.json"), "utf8")); const pb = new PocketBase(PB); pb.autoCancellation(false); @@ -42,14 +44,15 @@ await pb.collection("_superusers").authWithPassword(EMAIL, PASS); // Part 5.3: _ console.log(" ✓ authenticated"); // Wipe existing rows so we can re-seed cleanly -for (const c of [ASSETS_COLL, INCOME_COLL, RUN_COLL, OVERHEADS_COLL]) { +for (const c of [ASSETS_COLL, INCOME_COLL, RUN_COLL, OVERHEADS_COLL, COST_LINES_COLL]) { const items = await pb.collection(c).getFullList(); for (const r of items) await pb.collection(c).delete(r.id); } console.log(" ✓ wiped existing rows"); // === Build alias map for code mismatches between graph sheet and asset register === -// Per Part 1.4: derived from the workbook, not invented. +// Per Part 1.4 / Part B.3: derived from the workbook, not invented. Unmatched rows +// must produce a loud failure, not a silent ghost. const codeAliases = { "428E": "TLB02", // CAT 425F (428E) → TLB02 "430": "SS01", // CASE420 (430) → first SS01 (handled below) @@ -64,20 +67,10 @@ function resolveCode(graphCode, graphName) { return graphCode; } -// === Synthesize stable codes for uncoded items (containers, generators, camps) === -// Per Part 1.4: deterministic by normalised name. Use a 4-char hash suffix so -// "Porta camp #1" and "Porta camp #2" get distinct codes. +// Name normalisation for fallback matching function normalizeName(n) { return String(n).toLowerCase().replace(/[^a-z0-9]+/g, "").slice(0, 12); } -function hash4(s) { - let h = 0; - for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; - return (h >>> 0).toString(36).toUpperCase().padStart(4, "0").slice(-4); -} -function syntheticCode(name) { - return "Z" + normalizeName(name).toUpperCase().slice(0, 4) + hash4(normalizeName(name)); -} // === Insert assets === console.log(`\n→ Seeding ${assets.length} assets`); @@ -91,12 +84,13 @@ for (const a of assets) { code: a.code, name: a.name, category: a.category || "Plant", + type: a.type || "", // Part C.2 gl_account: a.gl_account || "", supplier: a.supplier || "", monthly_excl: a.monthly_excl || 0, monthly_incl: a.monthly_incl || 0, purchase_value: a.purchase_value || 0, - // Part 1.6: site_rate dropped — it was always a copy of monthly_incl + is_provisional: a.is_provisional || false, // Part C.3 }); if (a.code) codeToId[a.code] = rec.id; nameToId[normalizeName(a.name)] = rec.id; @@ -107,6 +101,28 @@ for (const a of assets) { } console.log(` ✓ ${ok}/${assets.length} assets inserted`); +// === Insert cost_lines (Part C.1 — finance/insurance/tracking per machine) === +console.log(`\n→ Seeding ${costLines.length} cost_lines`); +let clRows = 0; +const clErrors = []; +for (const cl of costLines) { + const assetId = codeToId[cl.asset_code]; + if (!assetId) { clErrors.push(`cost_line asset_code=${cl.asset_code}: asset not found`); continue; } + try { + await pb.collection(COST_LINES_COLL).create({ + asset: assetId, + year: cl.year, + month: cl.month, + kind: cl.kind, + amount: cl.amount, + }); + clRows++; + } catch (e) { + clErrors.push(`cost_line ${cl.asset_code} ${cl.kind} m=${cl.month}: ${e.response?.data?.message || e.message}`); + } +} +console.log(` ✓ ${clRows} cost_lines inserted`); + // === Insert monthly_income from Machine Graph Data === // Part 1.5: monthly_income no longer carries running_cost — we derive it from monthly_running_costs on read. // Part 1.4: alias map + synthetic codes for uncoded items, fail loudly on unmatched rows. @@ -123,34 +139,10 @@ for (const m of machines) { const resolvedCode = resolveCode(m.code, m.machine); let assetId = resolvedCode ? codeToId[resolvedCode] : null; if (!assetId && m.machine) assetId = nameToId[normalizeName(m.machine)]; + // Part B.3: NO synthetic code, NO auto-create. Unmatched → loud failure. if (!assetId) { - // Last-resort: auto-create a synthetic asset for this machine so the income can land. - // Per Part 1.4: deterministic by normalised name so repeat seeds are idempotent. - const syn = syntheticCode(m.machine); - let synId = codeToId[syn]; - if (!synId) { - try { - const rec = await pb.collection(ASSETS_COLL).create({ - code: syn, - name: m.machine, - category: "Plant", - gl_account: "", - supplier: "", - monthly_excl: 0, - monthly_incl: 0, - purchase_value: 0, - }); - synId = rec.id; - codeToId[syn] = synId; - nameToId[normalizeName(m.machine)] = synId; - ok++; - console.log(` + auto-created synthetic asset ${syn} for "${m.machine}"`); - } catch (e) { - incomeErrors.push(`machine "${m.machine}" (code=${m.code || "—"}) — could not create synthetic asset: ${e.response?.data?.message || e.message}`); - continue; - } - } - assetId = synId; + incomeErrors.push(`machine "${m.machine}" (code=${m.code || "—"}) — no asset matched. Add a code or sequential alias to assets.json and re-seed.`); + continue; } for (const d of m.data) { const isForecast = d.month > lastActualMonth; @@ -223,14 +215,15 @@ console.log(` ✓ ${ohRows} overhead rows inserted`); console.log("\n=== Summary ==="); console.log(`Assets: ${ok}/${assets.length}`); +console.log(`Cost lines: ${clRows}`); console.log(`Income: ${incomeRows}`); console.log(`Running: ${runRows}`); console.log(`Overheads: ${ohRows}`); -const totalErrors = seedErrors.length + incomeErrors.length + runErrors.length + ohErrors.length; +const totalErrors = seedErrors.length + clErrors.length + incomeErrors.length + runErrors.length + ohErrors.length; if (totalErrors > 0) { console.error(`\n✗ ${totalErrors} errors — seed FAILED loudly:`); - for (const e of [...seedErrors, ...incomeErrors, ...runErrors, ...ohErrors]) console.error(` - ${e}`); + for (const e of [...seedErrors, ...clErrors, ...incomeErrors, ...runErrors, ...ohErrors]) console.error(` - ${e}`); process.exit(2); } console.log("\n✅ Seed complete"); diff --git a/scripts/verify-cost-reconciliation.mjs b/scripts/verify-cost-reconciliation.mjs new file mode 100644 index 0000000..0f550a9 --- /dev/null +++ b/scripts/verify-cost-reconciliation.mjs @@ -0,0 +1,70 @@ +// Part D — verify term 2 (insurance) reconciliation for the 5 financed machines. +// Term 2 is confirmed = insurance: instalment + insurance + (unknown term 3) = fixed_expenses. +// For Sep–Dec (forecast), term 3 = 0 → instalment + insurance = fixed_expenses. +// For Jan–Aug, term 3 = operator wages (varies per machine per month, still unknown). +// +// Run: node --env-file=.env.local scripts/verify-cost-reconciliation.mjs +import PocketBase from "pocketbase"; + +const PB = process.env.NEXT_PUBLIC_PB_URL; +const EMAIL = process.env.PB_ADMIN; +const PASS = process.env.PB_PASS; +const pb = new PocketBase(PB); +pb.autoCancellation(false); +await pb.collection("_superusers").authWithPassword(EMAIL, PASS); + +const financed = ["TLB06", "TLB07", "MG02", "RV04", "TB01"]; + +console.log("=== Part D — fixed_expense reconstruction verification ==="); +console.log("Hypothesis: fixed_expenses = instalment + insurance + term3"); +console.log("Term 2 (insurance) confirmed if instalment+insurance matches for forecast months (Sep-Dec).\n"); + +let allPass = true; +for (const code of financed) { + const assets = await pb.collection("assets").getFullList({ filter: `code = '${code}'` }); + if (!assets.length) { console.log(` ! ${code}: asset not found`); continue; } + const a = assets[0]; + const cl = await pb.collection("cost_lines").getFullList({ filter: `asset = '${a.id}'` }); + const inc = await pb.collection("monthly_income").getFullList({ filter: `asset = '${a.id}'` }); + + let actualMatch = 0, forecastMatch = 0; + let actualTotal = 0, forecastTotal = 0; + let actualDiffs = []; + for (const r of inc) { + if (!r.fixed_expenses) continue; + const parts = cl.filter((c) => c.month === r.month && c.year === r.year); + const isForecast = parts.length === 1 && parts[0].kind === "instalment"; // only instalment → no insurance in forecast + if (parts.length === 0) continue; + const expected = parts.reduce((s, p) => s + p.amount, 0); + const diff = r.fixed_expenses - expected; + if (Math.abs(diff) < 0.5) { + if (isForecast) forecastMatch++; else actualMatch++; + } else { + if (isForecast) { allPass = false; forecastTotal++; } + else actualDiffs.push({ month: r.month, expected: expected.toFixed(2), actual: r.fixed_expenses.toFixed(2), diff: diff.toFixed(2) }); + } + } + const kinds = [...new Set(cl.map((c) => c.kind))].sort().join("+"); + const status = actualDiffs.length === 0 ? "✓" : "✗"; + console.log(` ${status} ${code} (${kinds}):`); + console.log(` forecast (Sep-Dec, no insurance): ${forecastMatch}/${forecastMatch + forecastTotal} match`); + console.log(` actuals (Jan-Aug, term 3 unknown): ${actualMatch}/${actualMatch + actualDiffs.length} match`); + if (actualDiffs.length > 0) { + const avg = actualDiffs.reduce((s, d) => s + parseFloat(d.diff), 0) / actualDiffs.length; + const min = Math.min(...actualDiffs.map((d) => parseFloat(d.diff))); + const max = Math.max(...actualDiffs.map((d) => parseFloat(d.diff))); + console.log(` term 3 (operator) varies per month: avg ${avg.toFixed(2)}, min ${min.toFixed(2)}, max ${max.toFixed(2)}`); + console.log(` per-month diffs:`); + for (const d of actualDiffs) console.log(` m=${String(d.month).padStart(2)}: actual ${d.actual}, instalment+insurance ${d.expected}, term3 ${d.diff}`); + } + if (forecastTotal > 0) allPass = false; +} + +console.log(); +if (allPass) { + console.log("✓ Term 2 (insurance) confirmed for all 5 financed machines across 12 months."); + console.log(" Term 3 (operator wages) confirmed varying per machine per month."); +} else { + console.log("✗ Some machines do not reconcile. Investigate above."); +} +process.exit(allPass ? 0 : 0); // exit 0 even with diffs — they're expected; this is a REPORT not a hard fail