App for the Mega Infrastructure AI Integration programme (A000) and Mega Plant (B110). 100 activities across 2 programmes, 06 Oct 2026 - 31 Aug 2027, parsed from the client PDF and frozen as an immutable baseline. Views: Dashboard, Timeline (month grid + week grid matching the PDF's Friday-anchored week numbering), Todo list, WBS, Data. Notes on the source data: - Hierarchy is derived from the PDF's indentation, not the activity code. Codes do not sort structurally (A490 sits under Workforce, A510 under Financials), so a code-ordered import would produce a wrong tree. - The schedule is built from the dates, not the PDF's "Durn" column, which disagrees with its own dates for 13 of the 100 activities. - Spec issues found in the PDF are surfaced in-app under Data: 2 unresolved "?" placeholders, 6 activities sharing one stretched 6-week window while declared 15 days, 13 duration mismatches, 17 spelling corrections (original wording preserved and viewable). Progress (status, % complete, note) is stored behind a pluggable adapter so the site can be static today and point at a shared JSON store later without touching the UI.
147 lines
5.4 KiB
Python
147 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate src/data/baseline.ts from the parsed Candy barchart dump."""
|
|
import json, datetime, re, textwrap
|
|
from collections import OrderedDict
|
|
|
|
acts = json.load(open("/tmp/mega_acts.json"))
|
|
by = {a["code"]: a for a in acts}
|
|
|
|
# ---- ordering: roots in document order, children in document order -------
|
|
roots = [a["code"] for a in acts if a["parent"] is None]
|
|
|
|
def walk(code, depth, path):
|
|
a = by[code]
|
|
a["level"] = depth
|
|
a["path"] = path
|
|
for c in a["children"]:
|
|
walk(c, depth + 1, path + [code])
|
|
for r in roots:
|
|
walk(r, 0, [])
|
|
|
|
# ---- typo corrections (display only; originalName is preserved) ----------
|
|
FIX = [
|
|
("Mega Infrastructure AI Intergration", "Mega Infrastructure AI Integration"),
|
|
("CCS Intergration", "CCS Integration"),
|
|
("Training & Cources", "Training & Courses"),
|
|
("Drivers license & PDP", "Drivers licence & PDP"),
|
|
("All sites income & Expences", "All sites income & Expenses"),
|
|
("All sites combined retetion", "All sites combined retention"),
|
|
("All sites combined Expences", "All sites combined Expenses"),
|
|
("All sites combined material expences", "All sites combined material expenses"),
|
|
("Combined supplier expences", "Combined supplier expenses"),
|
|
("Head office fixed expences", "Head office fixed expenses"),
|
|
("Expences per week", "Expenses per week"),
|
|
("Expences per month total to date", "Expenses per month total to date"),
|
|
("Opperator certificate", "Operator certificate"),
|
|
("Opperator", "Operator"),
|
|
("Expences", "Expenses"),
|
|
("VaT allocation", "VAT allocation"),
|
|
]
|
|
for a in acts:
|
|
a["originalName"] = a["name"]
|
|
n = a["name"]
|
|
for bad, good in FIX:
|
|
if n == bad:
|
|
n = good
|
|
break
|
|
if bad in n:
|
|
n = n.replace(bad, good)
|
|
a["displayName"] = n
|
|
|
|
# ---- quality analysis ----------------------------------------------------
|
|
def wd(a, b):
|
|
d, n = a, 0
|
|
while d <= b:
|
|
if d.weekday() < 5:
|
|
n += 1
|
|
d += datetime.timedelta(days=1)
|
|
return n
|
|
|
|
def dt(s): return datetime.date.fromisoformat(s)
|
|
|
|
for a in acts:
|
|
s, f = dt(a["start"]), dt(a["finish"])
|
|
a["spanDays"] = wd(s, f)
|
|
a["durMismatch"] = (a["duration"] != a["spanDays"])
|
|
|
|
# stretched-window cluster: leaves sharing an identical window that is ~2x their declared duration
|
|
from collections import defaultdict
|
|
win = defaultdict(list)
|
|
for a in acts:
|
|
win[(a["start"], a["finish"])].append(a["code"])
|
|
STRETCHED = set()
|
|
for k, v in win.items():
|
|
if len(v) < 2:
|
|
continue
|
|
for c in v:
|
|
a = by[c]
|
|
if a["durMismatch"] and a["duration"] > 0 and a["spanDays"] > a["duration"] * 1.5:
|
|
STRETCHED.add(c)
|
|
|
|
for a in acts:
|
|
a["unconfirmed"] = "?" in a["name"]
|
|
a["stretched"] = a["code"] in STRETCHED
|
|
a["typoFixed"] = a["displayName"] != a["originalName"]
|
|
a["workstream"] = a["path"][1] if len(a["path"]) >= 2 else None
|
|
|
|
PROG = {
|
|
"A000": "Mega Infrastructure AI Integration",
|
|
"B110": "Mega Plant",
|
|
}
|
|
|
|
def q(s):
|
|
return json.dumps(s).replace("\\", "\\\\") if False else json.dumps(s)
|
|
|
|
out = []
|
|
out.append("// AUTO-GENERATED from 'Mega Infrastructure AI intergration system.pdf'")
|
|
out.append("// (Candy 2.05p / P6 barchart export, printed 30 Sep 2026).")
|
|
out.append("// 100 activities, 2 programmes, 06 Oct 2026 -> 31 Aug 2027.")
|
|
out.append("// Hierarchy comes from PDF indentation -- NOT from the activity code.")
|
|
out.append("// Regenerate with scripts/gen_baseline.py if the source PDF changes.")
|
|
out.append("")
|
|
out.append("import type { BaselineActivity } from '../types'")
|
|
out.append("")
|
|
out.append("export const SOURCE_PDF = 'Mega Infrastructure AI intergration system.pdf'")
|
|
out.append("export const SOURCE_PRINTED = '30 September 2026 13:00'")
|
|
out.append("")
|
|
out.append("export const PROGRAMMES = [")
|
|
for r in roots:
|
|
a = by[r]
|
|
out.append(" {")
|
|
out.append(f" code: {q(a['code'])},")
|
|
out.append(f" name: {q(PROG[a['code']])},")
|
|
out.append(f" originalName: {q(a['originalName'])},")
|
|
out.append(f" start: {q(a['start'])},")
|
|
out.append(f" finish: {q(a['finish'])},")
|
|
out.append(" },")
|
|
out.append("] as const")
|
|
out.append("")
|
|
out.append("export const BASELINE: BaselineActivity[] = [")
|
|
for a in acts:
|
|
flags = []
|
|
if a["unconfirmed"]: flags.append("unconfirmed")
|
|
if a["stretched"]: flags.append("stretched")
|
|
if a["durMismatch"]: flags.append("duration-mismatch")
|
|
if a["typoFixed"]: flags.append("name-corrected")
|
|
out.append(" {")
|
|
out.append(f" code: {q(a['code'])},")
|
|
out.append(f" name: {q(a['displayName'])},")
|
|
out.append(f" originalName: {q(a['originalName'])},")
|
|
out.append(f" parent: {q(a['parent'])},")
|
|
out.append(f" level: {a['level']},")
|
|
out.append(f" plannedDuration: {a['duration']},")
|
|
out.append(f" spanDays: {a['spanDays']},")
|
|
out.append(f" start: {q(a['start'])},")
|
|
out.append(f" finish: {q(a['finish'])},")
|
|
out.append(f" flags: {json.dumps(flags)},")
|
|
out.append(" },")
|
|
out.append("]")
|
|
out.append("")
|
|
|
|
src = "\n".join(out)
|
|
open("/tmp/baseline.ts", "w").write(src)
|
|
print(f"generated {len(acts)} activities, {len(src)} bytes")
|
|
print(f"flags: unconfirmed={sum(1 for a in acts if a['unconfirmed'])} "
|
|
f"stretched={sum(1 for a in acts if a['stretched'])} "
|
|
f"durMismatch={sum(1 for a in acts if a['durMismatch'])} "
|
|
f"nameCorrected={sum(1 for a in acts if a['typoFixed'])}")
|