const currentMonthYear = window.moment().format("MMMM YYYY");
dv.header(1, currentMonthYear);
const lastSupervision = dv.current()?.["last supervision"];
 
if (lastSupervision) {
  const date = new Date(lastSupervision);
  const formatted = date.toLocaleDateString("en-GB", {
    day: "2-digit",
    month: "short",
    year: "numeric"
  });
  dv.paragraph("**Previous supervision:** " + formatted);
} else {
  dv.paragraph("⚠️ No 'last supervision' date found in this note.");
}

Report instructions

Adjust widths in supervision-report.css:

  • For client report: edit col:nth-child(N)
  • For issues report: edit col:nth-child(1)
  • To find the location of the css file, go to Settings/Appearance/CSS Snippets
  • File/Export to PDF… save in the Downloads folder

Current

// Supervision client report (scoped table class added at the end)
// Now includes "S" column for allocated-supervisor abbreviation.
// Shows Fq as "old -> new" if frequency has changed since last supervision.
// Shows Name as "old -> new" if name has changed since last supervision.
// Adds "I/O" column: 'I' if majority in-person; 'O' if majority online/remote.
// Columns: [S, Client Ref, Start Date, Name, Age, Gen, I/O, Fq, Total Hours, Issues]
 
// ── Inputs from this note ────────────────────────────────────────────────────
const supervisorRaw = dv.current()?.supervisor;
const lastSupervision = dv.current()?.["last supervision"];
 
if (!supervisorRaw || !lastSupervision) {
  dv.paragraph("⚠️ Missing 'supervisor' or 'last supervision' field in this note.");
} else {
  const supervisionDate = new Date(lastSupervision);
 
  // ── Helpers ───────────────────────────────────────────────────────────────
  const toArray = v => Array.isArray(v) ? v : (v == null ? [] : String(v).split(/[, \s]+/).filter(Boolean));
  const stripHash = t => String(t ?? "").replace(/^#/, "");
  const norm = t => stripHash(String(t ?? "")).trim().replace(/\s+/g, " ").toLowerCase();
 
  function tagSetOf(p) {
    const fm = toArray(p.tags ?? p.tag);
    const inline = (p.file?.tags ?? []).map(stripHash);
    return new Set([...fm, ...inline].map(norm));
  }
 
  const supervisorTag = norm(Array.isArray(supervisorRaw) ? supervisorRaw[0] : supervisorRaw);
 
  const getDates = raw => {
    const arr = Array.isArray(raw) ? raw : [raw];
    return arr
      .map(x => (x instanceof Date ? x : new Date(x)))
      .filter(d => d instanceof Date && !isNaN(d));
  };
 
  const minDate = raw => {
    const ds = getDates(raw);
    return ds.length ? new Date(Math.min(...ds.map(d => d.getTime()))) : null;
  };
 
  const maxDate = raw => {
    const ds = getDates(raw);
    return ds.length ? new Date(Math.max(...ds.map(d => d.getTime()))) : null;
  };
 
  const fmtDate = d =>
    (d instanceof Date && !isNaN(d))
      ? dv.luxon.DateTime.fromJSDate(d).toFormat("yyyy-LL-dd")
      : "";
 
  const coerceHours = h => {
    if (Array.isArray(h)) return Array.from(h).map(coerceHours).reduce((a,b)=>a+b, 0);
    if (typeof h === "number") return isFinite(h) ? h : 0;
    if (h == null) return 0;
    let txt = String(h).trim();
    txt = txt.replace("½","0.5").replace("¼","0.25").replace("¾","0.75");
    txt = txt.replace(/(\d+)\s*\/\s*(\d+)/, (_,a,b)=> (parseFloat(a)/parseFloat(b)).toString());
    const m = txt.match(/-?\d+(\.\d+)?/);
    const n = m ? parseFloat(m[0]) : NaN;
    return isNaN(n) ? 0 : n;
  };
 
  const isDuration = v => v && typeof v === "object" && dv?.luxon?.Duration?.isDuration?.(v);
 
  const formatDurationShort = dur => {
    const o = dur.shiftTo("weeks", "days", "hours", "minutes");
    const w = Math.floor(o.weeks || 0);
    const d = Math.floor(o.days || 0);
    const h = Math.floor(o.hours || 0);
    const m = Math.floor(o.minutes || 0);
    const parts = [];
    if (w) parts.push(`${w}w`);
    if (d) parts.push(`${d}d`);
    if (h) parts.push(`${h}h`);
    if (!parts.length && m) parts.push(`${m}m`);
    return parts.join(" ") || "0m";
  };
 
  const cleanFreqVal = v => {
    if (isDuration(v)) return formatDurationShort(v);
    if (typeof v === "string") {
      const isoW = v.match(/^P(\d+)W$/i);
      if (isoW) return `${isoW[1]}w`;
      return v;
    }
    if (typeof v === "number") return `${v}`;
    return String(v ?? "");
  };
 
  const coerceFrequency = f =>
    Array.isArray(f) ? f.map(cleanFreqVal).filter(Boolean).join(", ") : cleanFreqVal(f);
 
  const abbrevSupervisor = val => {
    const s = String(val ?? "").trim();
    if (!s) return "";
    const m = s.match(/^([A-Za-z]).*?-([A-Za-z])/);
    if (m) return (m[1] + m[2]).toUpperCase();
    return s[0]?.toUpperCase() ?? "";
  };
 
  const ONLINE_KEYS = [
    "online","writeupp-online","zoom","teams","microsoft teams","ms teams","google meet","meet",
    "skype","video","videocall","video call","telehealth","remote","virtual",
    "phone","telephone","call","whatsapp call","signal call","facetime"
  ];
 
  const INPERSON_KEYS = [
    "in person","in-person","in person.","f2f","face-to-face","face to face",
    "room","in the room","clinic","in clinic","office","practice"
  ];
 
  const hitsAny = (txt, keys) => keys.some(k => txt.includes(k));
 
  const classifyMedium = raw => {
    const tokens = toArray(raw).map(norm).filter(Boolean);
    if (!tokens.length) return null;
    const anyOnline = tokens.some(t => hitsAny(t, ONLINE_KEYS));
    const anyInPerson = tokens.some(t => hitsAny(t, INPERSON_KEYS));
    if (anyOnline && !anyInPerson) return "online";
    if (anyInPerson && !anyOnline) return "inperson";
    if (anyOnline && anyInPerson) return "online";
    return null;
  };
 
  const rows = Array.from(
    dv.pages('"100 CPD Record"')
      .where(p => p.clientref && tagSetOf(p).has("clienthours"))
      .groupBy(p => p.clientref),
    group => {
      const records = Array.from(group.rows ?? []);
 
      const validRecords = records
        .map(r => ({ r, firstDate: minDate(r.date), lastDate: maxDate(r.date) }))
        .filter(x => x.firstDate && x.lastDate);
 
      if (validRecords.length === 0) return null;
 
      const hasSupervisorTag = records.some(r => tagSetOf(r).has(supervisorTag));
      if (!hasSupervisorTag) return null;
 
      const isTerminated = records.some(r => r.terminate === true);
      const hasRecent = validRecords.some(x => x.lastDate > supervisionDate);
      if (isTerminated && !hasRecent) return null;
 
      const recentlyTerminated = isTerminated && hasRecent;
 
      const sorted = Array.from(validRecords).sort((a, b) => a.firstDate - b.firstDate);
 
      const first = sorted[0];
      const clientref = group.key + (recentlyTerminated ? " *" : "");
      const earliestDate = first.firstDate;
 
      let name = "", nameBefore = "", age = "", gender = "", issues = "";
      let frequency = "", frequencyBefore = "";
      let latestName = 0, latestNameBefore = 0, latestAge = 0, latestGender = 0, latestIssues = 0, latestFrequency = 0, latestBefore = 0;
      let allocSupRaw = "", latestAlloc = 0;
 
      let nOnline = 0, nInPerson = 0;
      let latestMediumClass = null, latestMediumDate = 0;
 
      for (let { r, lastDate } of validRecords) {
        const d = +lastDate;
 
        if (r.name) {
          const val = String(r.name).trim();
          if (d > latestName) {
            name = val;
            latestName = d;
          }
          if (d <= +supervisionDate && d > latestNameBefore) {
            nameBefore = val;
            latestNameBefore = d;
          }
        }
 
        if (r.age && d > latestAge) {
          age = r.age;
          latestAge = d;
        }
 
        if (r.gender && d > latestGender) {
          gender = r.gender;
          latestGender = d;
        }
 
        if (r.issues && d > latestIssues) {
          issues = Array.isArray(r.issues) ? r.issues.filter(Boolean).join(", ") : r.issues;
          latestIssues = d;
        }
 
        if (r.frequency != null) {
          const val = coerceFrequency(r.frequency);
          if (d > latestFrequency) {
            frequency = val ?? "";
            latestFrequency = d;
          }
          if (d <= +supervisionDate && d > latestBefore) {
            frequencyBefore = val ?? "";
            latestBefore = d;
          }
        }
 
        const asv = r["allocated-supervisor"];
        if (asv != null && d > latestAlloc) {
          allocSupRaw = Array.isArray(asv) ? (asv.length ? asv[asv.length - 1] : "") : asv;
          latestAlloc = d;
        }
 
        const cls = classifyMedium(r.medium);
        if (cls === "online") nOnline++;
        else if (cls === "inperson") nInPerson++;
 
        if (cls && d > latestMediumDate) {
          latestMediumClass = cls;
          latestMediumDate = d;
        }
      }
 
      const sAbbrev = abbrevSupervisor(allocSupRaw);
 
      const nameDisplay = (name && nameBefore && norm(name) !== norm(nameBefore))
        ? `${nameBefore} -> ${name}`
        : name || "";
 
      const fqDisplay = (frequency && frequencyBefore && norm(frequency) !== norm(frequencyBefore))
        ? `${frequencyBefore} -> ${frequency}`
        : frequency || "";
 
      const countedRecords = validRecords.filter(({ r }) => coerceHours(r.hours) > 0.5);
      
      const totalHours = Array.from(countedRecords, ({ r }) => coerceHours(r.hours))
  .reduce((a,b) => a + b, 0);
 
      let io = "";
      if (nInPerson > nOnline) io = "I";
      else if (nOnline > nInPerson) io = "O";
      else if (latestMediumClass) io = (latestMediumClass === "online" ? "O" : "I");
 
      return [
        sAbbrev,
        clientref,
        fmtDate(earliestDate),
        nameDisplay,
        age ?? "",
        gender ?? "",
        io,
        fqDisplay,
        +totalHours.toFixed(2),
        issues ?? ""
      ];
    }
  ).filter(Boolean)
    // Current report: supervisor abbreviation (S), then client name.
    .sort((a, b) => String(a[0] ?? "").localeCompare(String(b[0] ?? ""), undefined, { sensitivity: "base" })
      || String(a[3] ?? "").localeCompare(String(b[3] ?? ""), undefined, { sensitivity: "base" }));
 
  dv.table(
    ["S", "Client Ref", "Start Date", "Name", "Age", "Gen", "I/O", "Fq", "Total Hours", "Issues"],
    rows
  );
 
  const _t = dv.container.querySelectorAll("table");
  if (_t.length) _t[_t.length - 1].classList.add("supervision-client-report");
}

*Finished

// Report: Finished clients seen since last supervision
// Columns: [Client Ref, Name, Last Seen]
// Tagged as "supervision-finished" for CSS styling
 
const lastSupervision = dv.current()?.["last supervision"];
const supervisorRaw   = dv.current()?.supervisor;
 
if (!lastSupervision || !supervisorRaw) {
  dv.paragraph("⚠️ Missing 'last supervision' or 'supervisor' field in this note.");
} else {
  const supervisionDate = new Date(lastSupervision);
 
  // --- Tag normalisation ---------------------------------------------------
  const toArray   = v => Array.isArray(v) ? v : (v == null ? [] : String(v).split(/[, \s]+/).filter(Boolean));
  const stripHash = t => String(t ?? "").replace(/^#/, "");
  const norm      = t => stripHash(String(t ?? "")).toLowerCase();
 
  function tagSetOf(p) {
    const fm     = toArray(p.tags ?? p.tag);
    const inline = (p.file?.tags ?? []).map(stripHash);
    return new Set([...fm, ...inline].map(norm));
  }
 
  const supervisorTag = norm(Array.isArray(supervisorRaw) ? supervisorRaw[0] : supervisorRaw);
 
  // --- Build results -------------------------------------------------------
  const results = Array.from(
    dv.pages('"100 CPD Record"')
      .where(p => p.clientref && tagSetOf(p).has("clienthours"))
      .groupBy(p => p.clientref),
    group => {
      const records = Array.from(group.rows ?? []).filter(p => p.date);
      if (records.length === 0) return null;
 
      // must include supervisor tag OR 'finished'
      const hasRequiredTag = records.some(r => {
        const tags = tagSetOf(r);
        return tags.has(supervisorTag) || tags.has("finished");
      });
      if (!hasRequiredTag) return null;
 
      // must have at least one terminated record
      const hasTerminated = records.some(r => r.terminate === true);
 
      // must have at least one record after supervision
      const hasRecent = records.some(r => {
        const dates = toArray(r.date).map(d => new Date(d)).filter(d => d instanceof Date && !isNaN(d));
        return dates.some(d => d > supervisionDate);
      });
 
      if (!(hasTerminated && hasRecent)) return null;
 
      // find most recent date overall
      let mostRecentDate = new Date(0);
      let lastSeenDate   = null;
      for (let r of records) {
        const dates = toArray(r.date).map(d => new Date(d)).filter(d => !isNaN(d));
        for (let d of dates) {
          if (d > mostRecentDate) {
            mostRecentDate = d;
            lastSeenDate   = d;
          }
        }
      }
 
      // most recent record with a name
      let mostRecentNamed = null;
      let latestNamedDate = new Date(0);
      for (let r of records) {
        if (r.name) {
          const dates = toArray(r.date).map(d => new Date(d)).filter(d => !isNaN(d));
          for (let d of dates) {
            if (d > latestNamedDate) {
              latestNamedDate = d;
              mostRecentNamed = r;
            }
          }
        }
      }
 
      const clientref = group.key;
      const name      = mostRecentNamed?.name ?? "";
      const lastSeen  = lastSeenDate ? dv.luxon.DateTime.fromJSDate(lastSeenDate).toFormat("yyyy-LL-dd") : "";
 
      return [clientref, name, lastSeen];
    }
  ).filter(Boolean)
    .sort((a, b) => String(a[1] ?? "").localeCompare(String(b[1] ?? ""), undefined, { sensitivity: "base" })
      || String(a[0] ?? "").localeCompare(String(b[0] ?? ""), undefined, { sensitivity: "base" }));
 
  // --- Render --------------------------------------------------------------
  if (results.length === 0) {
    dv.paragraph("none");
  } else {
    dv.table(["Client Ref", "Name", "Last Seen"], results);
 
    // Tag last table with class + colgroup
    const tables = dv.container.querySelectorAll("table");
    const t = tables[tables.length - 1];
    if (t) {
      t.classList.add("supervision-finished");
      if (!t.querySelector("colgroup")) {
        const cg = document.createElement("colgroup");
        for (let i = 0; i < 3; i++) cg.appendChild(document.createElement("col"));
        t.insertBefore(cg, t.firstChild);
      }
      t.style.tableLayout = "fixed";
      t.style.width = "100%";
    }
  }
}

New

// Report: New clients since last supervision
// Columns: [Client Ref, Name, Issues, Referred By]
// Tagged as "supervision-new" for CSS styling
 
const lastSupervision = dv.current()?.["last supervision"];
const supervisorRaw   = dv.current()?.supervisor;
 
if (!lastSupervision || !supervisorRaw) {
  dv.paragraph("⚠️ Missing 'last supervision' or 'supervisor' field in this note.");
} else {
  const supervisionDate = new Date(lastSupervision);
 
  // --- Tag normalisation ---------------------------------------------------
  const toArray   = v => Array.isArray(v) ? v : (v == null ? [] : String(v).split(/[, \s]+/).filter(Boolean));
  const stripHash = t => String(t ?? "").replace(/^#/, "");
  const norm      = t => stripHash(String(t ?? "")).toLowerCase();
 
  function tagSetOf(p) {
    const fm     = toArray(p.tags ?? p.tag);
    const inline = (p.file?.tags ?? []).map(stripHash);
    return new Set([...fm, ...inline].map(norm));
  }
 
  const supervisorTag = norm(Array.isArray(supervisorRaw) ? supervisorRaw[0] : supervisorRaw);
 
  // --- Build results -------------------------------------------------------
  const results = Array.from(
    dv.pages('"100 CPD Record"')
      .where(p => p.clientref && tagSetOf(p).has("clienthours"))
      .groupBy(p => p.clientref),
    group => {
      const records = Array.from(group.rows ?? []).filter(p => p.date);
      if (records.length === 0) return null;
 
      // must have supervisor tag OR 'new'
      const hasRequiredTag = records.some(r => {
        const tags = tagSetOf(r);
        return tags.has(supervisorTag) || tags.has("new");
      });
      if (!hasRequiredTag) return null;
 
      // earliest record date
      let earliestDate = new Date(8640000000000000);
      for (let r of records) {
        const dates = toArray(r.date).map(d => new Date(d)).filter(d => !isNaN(d));
        for (let d of dates) {
          if (d < earliestDate) earliestDate = d;
        }
      }
 
      // only include clients whose earliest record is on/after supervision date
      if (earliestDate < supervisionDate) return null;
 
      // most recent values for fields
      let name = "", issues = "", referred = "";
      let latestNameDate = new Date(0), latestIssuesDate = new Date(0), latestRefDate = new Date(0);
 
      for (let r of records) {
        const dates = toArray(r.date).map(d => new Date(d)).filter(d => !isNaN(d));
        const parsed = dates.length ? dates[0] : null;
        if (!parsed) continue;
 
        if (r.name && parsed > latestNameDate) {
          name = r.name;
          latestNameDate = parsed;
        }
        if (r.issues && parsed > latestIssuesDate) {
          issues = r.issues;
          latestIssuesDate = parsed;
        }
        if (r["referred by"] && parsed > latestRefDate) {
          referred = r["referred by"];
          latestRefDate = parsed;
        }
      }
 
      return [group.key, name, issues, referred];
    }
  ).filter(Boolean)
    .sort((a, b) => String(a[1] ?? "").localeCompare(String(b[1] ?? ""), undefined, { sensitivity: "base" })
      || String(a[0] ?? "").localeCompare(String(b[0] ?? ""), undefined, { sensitivity: "base" }));
 
  // --- Render --------------------------------------------------------------
  if (results.length === 0) {
    dv.paragraph("none");
  } else {
    dv.table(["Client Ref", "Name", "Issues", "Referred By"], results);
 
    // Tag last table with class + colgroup
    const tables = dv.container.querySelectorAll("table");
    const t = tables[tables.length - 1];
    if (t) {
      t.classList.add("supervision-new");
      if (!t.querySelector("colgroup")) {
        const cg = document.createElement("colgroup");
        for (let i = 0; i < 4; i++) cg.appendChild(document.createElement("col"));
        t.insertBefore(cg, t.firstChild);
      }
      t.style.tableLayout = "fixed";
      t.style.width = "100%";
    }
  }
}

Ongoing

// Report: Latest "Current Issues" per client (widths controlled via CSS)
// Columns: [Client Ref, Name, Current Issues]
 
const lastSupervision = dv.current()?.["last supervision"];
const supervisorRaw   = dv.current()?.supervisor; // can be "#name", "name", array, etc.
 
if (!lastSupervision || !supervisorRaw) {
  dv.paragraph("⚠️ Missing 'last supervision' or 'supervisor' field in this note.");
} else {
  const supervisionDate = new Date(lastSupervision);
 
  // --- Tag normalisation helpers -------------------------------------------
  const toArray   = v => Array.isArray(v) ? v : (v == null ? [] : String(v).split(/[, \s]+/).filter(Boolean));
  const stripHash = t => String(t ?? "").replace(/^#/, "");
  const norm      = t => stripHash(String(t ?? "")).toLowerCase();
 
  function tagSetOf(p) {
    const fm     = toArray(p.tags ?? p.tag);
    const inline = (p.file?.tags ?? []).map(stripHash);
    return Array.from(new Set([...fm, ...inline].map(norm)));
  }
 
  const supervisorTag = norm(Array.isArray(supervisorRaw) ? supervisorRaw[0] : supervisorRaw);
 
  // --- Date helpers ---------------------------------------------------------
  function getDates(raw) {
    const arr = Array.isArray(raw) ? raw : [raw];
    return arr.map(x => new Date(x)).filter(d => !isNaN(d));
  }
  function latestDate(raw) {
    const ds = getDates(raw);
    return ds.length ? new Date(Math.max(...ds.map(d => d.getTime()))) : null;
  }
 
  // --- Build table rows -----------------------------------------------------
  const rows = Array.from(
    dv.pages('"100 CPD Record"')
      .where(p => tagSetOf(p).includes("clienthours"))
      .groupBy(p => p.clientref),
    group => {
      const records = Array.from(group.rows ?? []).filter(p => p.date);
      if (records.length === 0) return null;
 
      const hasSupervisorTagged = records.some(r => tagSetOf(r).includes(supervisorTag));
      if (!hasSupervisorTagged) return null;
 
      const isTerminated = records.some(r => r.terminate === true);
      const latestOverallDate = records.reduce((latest, r) => {
        const d = latestDate(r.date);
        return d && d > latest ? d : latest;
      }, new Date(0));
      if (isTerminated && latestOverallDate < supervisionDate) return null;
 
      // Most recent name
      let name = "";
      let latestNameDate = new Date(0);
      for (let r of records) {
        if (r.name) {
          const d = latestDate(r.date);
          if (d && d > latestNameDate) {
            latestNameDate = d;
            name = r.name;
          }
        }
      }
 
      // Current issues: prefer latest after supervision; otherwise latest overall
      const issueRecords = records
        .filter(r => r["current issues"])
        .map(r => {
          const d = latestDate(r.date);
          const val = Array.isArray(r["current issues"])
            ? r["current issues"][r["current issues"].length - 1]
            : r["current issues"];
          return { date: d, value: val };
        })
        .filter(r => r.date);
 
      if (issueRecords.length === 0) return [group.key, name, ""];
 
      const postSupervision = issueRecords
        .filter(r => r.date > supervisionDate)
        .sort((a, b) => b.date - a.date);
 
      const currentIssues = postSupervision.length > 0
        ? postSupervision[0].value
        : issueRecords.sort((a, b) => b.date - a.date)[0].value;
 
      return [group.key, name, currentIssues];
    }
  ).filter(Boolean)
    .sort((a, b) => String(a[1] ?? "").localeCompare(String(b[1] ?? ""), undefined, { sensitivity: "base" })
      || String(a[0] ?? "").localeCompare(String(b[0] ?? ""), undefined, { sensitivity: "base" }));
 
  // --- Render table ---------------------------------------------------------
  dv.table(["Client Ref", "Name", "Current Issues"], rows);
 
  // Tag the just-rendered table and inject a <colgroup> with 3 <col>s (no widths)
  const tables = dv.container.querySelectorAll("table");
  const t = tables[tables.length - 1];
  if (t) {
    t.classList.add("supervision-issues-latest");  // unique class for CSS targeting
    if (!t.querySelector("colgroup")) {
      const cg = document.createElement("colgroup");
      for (let i = 0; i < 3; i++) cg.appendChild(document.createElement("col"));
      t.insertBefore(cg, t.firstChild);
    }
    // Optional, but helps PDF export respect widths
    t.style.tableLayout = "fixed";
    t.style.width = "100%";
  }
}
### Sessions since last supervision
// Report: All current issues since last supervision (per client)
// This version creates <col> elements but leaves widths to CSS only.
 
const lastSupervision = dv.current()?.["last supervision"];
const supervisorRaw   = dv.current()?.supervisor;
 
if (!lastSupervision || !supervisorRaw) {
  dv.paragraph("⚠️ Missing 'last supervision' or 'supervisor' field in this note.");
} else {
  const supervisionDate = new Date(lastSupervision);
 
  // ── Tag normalisation ──────────────────────────────────────────────────
  const toArray   = v => Array.isArray(v) ? v : (v == null ? [] : String(v).split(/[, \s]+/).filter(Boolean));
  const stripHash = t => String(t ?? "").replace(/^#/, "");
  const norm      = t => stripHash(String(t ?? "")).toLowerCase();
 
  function tagSetOf(p) {
    const fm     = toArray(p.tags ?? p.tag);
    const inline = (p.file?.tags ?? []).map(stripHash);
    return Array.from(new Set([...fm, ...inline].map(norm)));
  }
 
  const supervisorTag = norm(Array.isArray(supervisorRaw) ? supervisorRaw[0] : supervisorRaw);
 
  // ── Date helpers ───────────────────────────────────────────────────────
  function getDates(raw) {
    const arr = Array.isArray(raw) ? raw : [raw];
    return arr.map(x => new Date(x)).filter(d => d instanceof Date && !isNaN(d));
  }
 
  function latestDate(raw) {
    const ds = getDates(raw);
    return ds.length ? new Date(Math.max(...ds.map(d => d.getTime()))) : null;
  }
 
  const fmt = d => {
    if (!(d instanceof Date) || isNaN(+d)) return "";
    const y  = d.getFullYear();
    const m  = String(d.getMonth() + 1).padStart(2, "0");
    const da = String(d.getDate()).padStart(2, "0");
    return `${y}-${m}-${da}`;
  };
 
  // ── Build per-client groups ────────────────────────────────────────────
  const clientGroups = Array.from(
    dv.pages('"100 CPD Record"')
      .where(p => tagSetOf(p).includes("clienthours"))
      .groupBy(p => p.clientref),
    group => {
      const records = Array.from(group.rows ?? []).filter(p => p.date);
      if (records.length === 0) return null;
 
      // Supervisor filter
      const hasSupervisorTagged = records.some(r => tagSetOf(r).includes(supervisorTag));
      if (!hasSupervisorTagged) return null;
 
      // Termination logic
      const isTerminated = records.some(r => r.terminate === true);
      const latestOverallDate = records.reduce((latest, r) => {
        const d = latestDate(r.date);
        return d && d > latest ? d : latest;
      }, new Date(0));
      if (isTerminated && latestOverallDate < supervisionDate) return null;
 
      // Most recent name
      let name = "";
      let latestNameDate = new Date(0);
      for (let r of records) {
        if (r.name) {
          const d = latestDate(r.date);
          if (d && d > latestNameDate) {
            latestNameDate = d;
            name = r.name;
          }
        }
      }
 
      // Gather all issues since supervision
      const issueRows = [];
      for (let r of records) {
        if (!r["current issues"]) continue;
        const datesAfter = getDates(r.date).filter(d => d > supervisionDate);
        if (datesAfter.length === 0) continue;
 
        const issueDate = new Date(Math.max(...datesAfter.map(d => d.getTime())));
        const issuesArr = Array.isArray(r["current issues"]) ? r["current issues"] : [r["current issues"]];
        for (const issue of issuesArr) {
          issueRows.push([fmt(issueDate), issue]);
        }
      }
 
      if (issueRows.length === 0) return null;
 
      // Sort issues per client by date (ascending)
      issueRows.sort((a, b) => new Date(a[0]) - new Date(b[0]));
 
      return {
        clientref: group.key,
        name: name,
        issues: issueRows
      };
    }
  ).filter(Boolean)
    .sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? ""), undefined, { sensitivity: "base" })
      || String(a.clientref ?? "").localeCompare(String(b.clientref ?? ""), undefined, { sensitivity: "base" }));
 
  // ── Render ─────────────────────────────────────────────────────────────
  dv.paragraph(`Listing all current issues since: **${fmt(supervisionDate)}**`);
 
  if (clientGroups.length === 0) {
    dv.paragraph("No current issues recorded since the last supervision.");
  } else {
    for (const g of clientGroups) {
      dv.header(3, `${g.clientref} — ${g.name}`);
      dv.table(["Issue Date", "Current Issues"], g.issues);
 
      // Tag the most recently rendered table and inject a <colgroup> with 2 <col>s.
      // No inline widths: CSS (supervision-report.css) controls sizing.
      const tables = dv.container.querySelectorAll("table");
      const t = tables[tables.length - 1];
      if (t) {
        t.classList.add("supervision-issues-table");
 
        if (!t.querySelector("colgroup")) {
          const cg = document.createElement("colgroup");
          // Create two columns: date + issues (widths via CSS)
          for (let i = 0; i < 2; i++) {
            cg.appendChild(document.createElement("col"));
          }
          t.insertBefore(cg, t.firstChild);
        }
      }
    }
  }
}

Summary of Client and Supervision Hours

// ─── Configuration ───
// 0) Minimum session length (client hours) to count:
const minClientHours = 0.5;      // only count entries where p.hours > this
// 1) absolute-hour limit for non-UKCP supervision (BACP ceiling):
const maxNonUkcpHours   = 20;    // e.g. 20
// 2) percentage-limit (of total supervision) for non-UKCP:
const maxNonUkcpPercent = 100;   // e.g. 100% means no extra cap by percent
 
// ─── Date range ───
const startDate = new Date(`${dv.current()["Start Date"]}`);
const endDate   = new Date(`${dv.current()["End Date"]}`);
 
// ─── Safe number formatter ───
const fmt = n => {
  const num = Number(n);
  return isNaN(num) ? "N/A" : num.toFixed(2);
};
 
// ─── Helper to check array-or-single dates ───
function inRange(p) {
  const dates = Array.isArray(p.date) ? p.date : [p.date];
  return dates.some(d => {
    const dt = new Date(d);
    return dt >= startDate && dt <= endDate;
  });
}
 
// ─── 1) Sum client hours (only where hours > minClientHours) ───
 
let grandTotalClient = 0;
 
dv.pages('"100 CPD Record" and #clienthours')
 
  .where(inRange)
 
  .forEach(p => {
 
    const hrs = Number(p.hours || 0);
 
    if (!(hrs > minClientHours)) return; // strictly greater than threshold
 
  
 
    const dates = Array.isArray(p.date) ? p.date : [p.date];
 
    dates.forEach(d => {
 
      const dt = new Date(d);
 
      if (dt >= startDate && dt <= endDate) {
 
        grandTotalClient += hrs;
 
      }
 
    });
 
  });
 
// ─── 2) Sum supervision & UKCP hours (excluding peer supervision) ───
let grandTotalSup = 0, ukcpTotal = 0;
let formatTotals = { "1-to-1": 0, "group": 0, "peer": 0, "fishbowl": 0, "unspecified": 0 };
 
dv.pages('"100 CPD Record" and #supervision')
  .where(inRange)
  .forEach(p => {
    const dates = Array.isArray(p.date) ? p.date : [p.date];
    dates.forEach(d => {
      const dt = new Date(d);
      if (dt >= startDate && dt <= endDate) {
        const hrs = p.hours || 0;
        const rawFmt = (p.format || "unspecified").toString().toLowerCase();
        const allowed = ["1-to-1", "group", "peer", "fishbowl"];
        const normalised = allowed.includes(rawFmt) ? rawFmt : "unspecified";
 
        // Always record format hours
        formatTotals[normalised] = (formatTotals[normalised] || 0) + hrs;
 
        // Only include non-peer hours in totals
        if (normalised !== "peer") {
          grandTotalSup += hrs;
          if ((p.Body || "").toLowerCase() === "ukcp") {
            ukcpTotal += hrs;
          }
        }
      }
    });
  });
 
// ─── 3) Compute non-UKCP hours & percent ───
const nonUkcpTotal   = grandTotalSup - ukcpTotal;
const nonUkcpPercent = grandTotalSup > 0
  ? (nonUkcpTotal / grandTotalSup) * 100
  : NaN;
 
// ─── 4) Warnings ───
 
 
// ─── 5) Determine allowed non-UKCP “actual” threshold ───
const p = maxNonUkcpPercent / 100;
const percentThresholdHours = ((1 / (1 - p)) - 1) * ukcpTotal;
 
let nonUkcpThreshold;
if (nonUkcpTotal <= maxNonUkcpHours && nonUkcpPercent <= maxNonUkcpPercent) {
  nonUkcpThreshold = nonUkcpTotal;
} else {
  nonUkcpThreshold = Math.min(maxNonUkcpHours, percentThresholdHours);
}
 
// ─── 6) Compute Current Actual Ratio ───
const currentActualRatio = (ukcpTotal + nonUkcpThreshold) > 0
  ? fmt(grandTotalClient / (ukcpTotal + nonUkcpThreshold))
  : "N/A";
 
// ─── 7) Other ratios ───
const ratioAll  = grandTotalSup    > 0 ? fmt(grandTotalClient / grandTotalSup) : "N/A";
const ratioUKCP = ukcpTotal        > 0 ? fmt(grandTotalClient / ukcpTotal)   : "N/A";
 
// ─── 8) Render header & thresholds ───
dv.header(2, "Supervision Summary & Ratios");
 
// ─── 9) Render main table ───
const formatRows = Object.entries(formatTotals).map(
  ([f, hrs]) => [`Supervision (${f})`, fmt(hrs)]
);
 
dv.table(
  ["Calculation", "Result"],
  [
    ["**Grand Total Client Hours**", fmt(grandTotalClient)],
    ["**Total Allowable Supervision Hours (exc. peer)**", fmt(ukcpTotal + nonUkcpThreshold)],
    ["**Current Actual Ratio (Client ÷ (UKCP + allowed non-UKCP))**", currentActualRatio]
  ]
);
 
 
Link to original