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%";
}
}// 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
Link to original // ─── 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] ] );