Free Resource
Private Practice Set-Up Guide
Two Google Apps Scripts to build your complete practice infrastructure in under 60 seconds. Run Step 1 once to create your Google Drive folder structure and all your documents. Then open your Client_Index spreadsheet and install Step 2 to get the Practice Tools menu.
Drive Setup
Go to script.google.com, create a new project, paste this script, fill in your details in the CONFIG section, then click Run → runDriveSetup.
/**
* ============================================================
* PRACTICE INFRASTRUCTURE — STEP 1: DRIVE SETUP
* Version 3.2
* ============================================================
* Run this ONCE from script.google.com to build your
* complete Google Drive folder structure and all documents.
*
* BEFORE RUNNING:
* 1. Fill in your details in the CONFIGURATION section below
* 2. Click Run → runDriveSetup
* 3. Approve permissions when prompted
* 4. Wait ~60 seconds for everything to build
* 5. Open Google Drive — find your Practice Infrastructure folder
*
* THEN: Open the Client_Index spreadsheet and follow the
* instructions inside to install the Practice Tools menu.
*
* Run this script ONCE only. Running again creates duplicates.
* ============================================================
*/
// ============================================================
// CONFIGURATION — Fill in before running
// ============================================================
var CONFIG = {
practiceName : "My Practice", // e.g. "Sarah Jones Therapy"
therapistName : "Your Name", // e.g. "Sarah Jones"
practiceEmail : "hello@mypractice.com", // Your Workspace email
country : "UK", // "UK", "Ireland" or "US"
defaultFee : 80, // Your standard session fee (number only)
currency : "£", // "£", "€" or "$"
sessionLength : 50, // Standard session length in minutes
version : "3.2"
};
var SETUP_DATE = new Date().toDateString();
// ============================================================
// MAIN — Run this
// ============================================================
function runDriveSetup() {
log("================================================");
log("Practice Infrastructure Setup v" + CONFIG.version);
log("Practice: " + CONFIG.practiceName);
log("Country: " + CONFIG.country);
log("Date: " + SETUP_DATE);
log("================================================");
var root = DriveApp.getRootFolder();
var practiceFolder = createFolder(root, "🏥 " + CONFIG.practiceName + " — Practice Infrastructure");
// Top level folders
var clinical = createFolder(practiceFolder, "1_Clinical_Records");
var admin = createFolder(practiceFolder, "2_Administrative");
var finance = createFolder(practiceFolder, "3_Finance");
var templates = createFolder(practiceFolder, "4_Templates_Policies");
var archives = createFolder(practiceFolder, "5_Archives");
// Build each section
buildStartHere(practiceFolder);
buildClinical(clinical);
buildAdministrative(admin, practiceFolder);
buildFinance(finance);
buildTemplates(templates);
buildArchives(archives);
log("================================================");
log("✅ DRIVE SETUP COMPLETE");
log("Next step: Open Client_Index in 2_Administrative");
log("Go to Extensions → Apps Script → paste STEP2 script");
log("================================================");
}
// ============================================================
// ⭐ START HERE
// ============================================================
function buildStartHere(parent) {
var doc = DocumentApp.create("⭐ START HERE — Your Practice Infrastructure Guide");
var b = doc.getBody();
h1(b, "Welcome to Your Practice Infrastructure");
b.appendParagraph("Practice: " + CONFIG.practiceName);
b.appendParagraph("Therapist: " + CONFIG.therapistName);
b.appendParagraph("Set up: " + SETUP_DATE + " | Version: " + CONFIG.version);
b.appendParagraph("");
h2(b, "What This System Is");
b.appendParagraph("This Google Drive folder is your complete practice management system. Everything related to your practice — client records, session notes, finance, policies and templates — lives here in one secure, organised place in your own Google Workspace account. Nobody else has access unless you grant it.");
b.appendParagraph("");
h2(b, "Your 5 Folders");
bold(b, "📁 1_Clinical_Records");
b.appendParagraph("One folder per client. Session notes, consent forms, intake forms and correspondence. Your most sensitive folder — never share publicly.");
b.appendParagraph("");
bold(b, "📁 2_Administrative");
b.appendParagraph("Your Client Index spreadsheet — the control centre for your entire practice. Contains client records, session log, billing, supervision log, CPD log, income dashboard and all automation tools.");
b.appendParagraph("");
bold(b, "📁 3_Finance");
b.appendParagraph("Invoices, receipts, Stripe summaries and tax documents. Safe to share with an accountant — this folder only, never Clinical Records.");
b.appendParagraph("");
bold(b, "📁 4_Templates_Policies");
b.appendParagraph("Consent form, privacy notice, intake form, session note, superbill, insurance receipt, referral letter, ending summary and all compliance policies. Always copy — never write in the originals.");
b.appendParagraph("");
bold(b, "📁 5_Archives");
b.appendParagraph("Closed client folders. Move here when therapy ends. Records stay until retention deadline, then permanently deleted and logged.");
b.appendParagraph("");
h2(b, "Your Two-Step Setup");
bold(b, "Step 1 — Drive Setup (done)");
b.appendParagraph("Your folder structure and all documents have been created.");
b.appendParagraph("");
bold(b, "Step 2 — Install Practice Tools Menu");
b.appendParagraph("Open 2_Administrative → Client_Index. Go to Extensions → Apps Script. Paste the STEP2 script provided by your practice support. Click Save, then Run → installPracticeTools. Reload the spreadsheet — you will see a Practice Tools menu appear.");
b.appendParagraph("");
b.appendParagraph("Once installed, use Practice Tools for everything: adding clients, logging sessions, generating superbills, closing cases and viewing your income dashboard.");
b.appendParagraph("");
h2(b, "Daily Workflow");
b.appendParagraph("After each session: Practice Tools → Log Session");
b.appendParagraph("New client: Practice Tools → Add New Client");
b.appendParagraph("End of month: Practice Tools → Generate Superbill / Invoice");
b.appendParagraph("Closing a case: Practice Tools → Close Client Case");
b.appendParagraph("Annual review: 4_Templates_Policies → Policies → Annual Compliance Checklist");
b.appendParagraph("");
h2(b, "Security Rules");
b.appendParagraph("✅ Always use your Workspace email — never personal Gmail — for client communication");
b.appendParagraph("✅ Keep two-factor authentication enabled on your Google account at all times");
b.appendParagraph("✅ Never share client folders publicly or via a shareable link");
b.appendParagraph("✅ Never discuss clinical content via WhatsApp, SMS or social media");
b.appendParagraph("✅ If you suspect unauthorised access, change your password immediately");
b.appendParagraph("");
h2(b, "If a Data Breach Occurs");
b.appendParagraph("1. Note date and time of discovery");
b.appendParagraph("2. Secure your account — change password, check active sessions in Google");
b.appendParagraph("3. Assess what data may be affected");
b.appendParagraph("4. Notify supervisory authority within 72 hours if personal data was involved:");
b.appendParagraph(" UK: ico.org.uk | Ireland: dataprotection.ie | US: hhs.gov/hipaa");
b.appendParagraph("5. Notify affected clients without undue delay if they are at risk");
b.appendParagraph("6. Document everything in 2_Administrative → Data_Breach_Log");
b.appendParagraph("");
b.appendParagraph("Last updated: " + SETUP_DATE);
doc.saveAndClose();
moveFile(doc.getId(), parent);
log("Created: ⭐ START HERE");
}
// ============================================================
// 1. CLINICAL RECORDS
// ============================================================
function buildClinical(parent) {
var example = createFolder(parent, "C001-EXAMPLE — Copy this folder for each new client");
createFolder(example, "01_Admin");
createFolder(example, "02_Consent_Forms");
createFolder(example, "03_Intake_Forms");
createFolder(example, "04_Session_Notes");
createFolder(example, "05_Client_Materials");
createFolder(example, "06_Correspondence");
var doc = DocumentApp.create("📋 Clinical Records — Quick Reference");
var b = doc.getBody();
h1(b, "Clinical Records — Quick Reference");
b.appendParagraph("One sub-folder per client, named: ClientCode-Initials");
b.appendParagraph("Example: C002-AB = client code C002, initials A.B.");
b.appendParagraph("");
bold(b, "File naming convention:");
b.appendParagraph("YYYY-MM-DD_C001_SessionNote");
b.appendParagraph("YYYY-MM-DD_C001_ConsentForm");
b.appendParagraph("YYYY-MM-DD_C001_IntakeForm");
b.appendParagraph("");
bold(b, "Sub-folders:");
b.appendParagraph("01_Admin — referral letters, appointment log");
b.appendParagraph("02_Consent_Forms — signed consent and privacy notice");
b.appendParagraph("03_Intake_Forms — completed intake responses");
b.appendParagraph("04_Session_Notes — one note per session, dated");
b.appendParagraph("05_Client_Materials — resources and worksheets");
b.appendParagraph("06_Correspondence — emails and letters");
b.appendParagraph("");
b.appendParagraph("⚠️ Special category health data. Never share publicly. Never move to a personal account.");
doc.saveAndClose();
moveFile(doc.getId(), parent);
log("Created: Clinical Records");
}
// ============================================================
// 2. ADMINISTRATIVE — Folder + Client Index Spreadsheet
// ============================================================
function buildAdministrative(parent, practiceFolder) {
createFolder(parent, "Practice_Policies");
createFolder(parent, "Insurance_Referrals");
createFolder(parent, "Forms_Not_Tied_To_Client");
createFolder(parent, "Subject_Access_Requests");
createFolder(parent, "Data_Breach_Log");
var ss = SpreadsheetApp.create("Client_Index — " + CONFIG.practiceName);
// ── Tab: Settings ──
buildSettingsTab(ss);
// ── Tab: Clients ──
buildClientsTab(ss);
// ── Tab: Waitlist ──
buildWaitlistTab(ss);
// ── Tab: Session_Log ──
buildSessionLogTab(ss);
// ── Tab: Income_Dashboard ──
buildIncomeDashboardTab(ss);
// ── Tab: Supervision_Log ──
buildSupervisionLogTab(ss);
// ── Tab: CPD_Log ──
buildCPDLogTab(ss);
// ── Tab: DNA_Log ──
buildDNALogTab(ss);
// ── Tab: Deletion_Log ──
buildDeletionLogTab(ss);
// ── Tab: SAR_Log ──
buildSARLogTab(ss);
// ── Tab: How To Use ──
buildHowToUseTab(ss);
// Remove default blank sheet if present
try {
var defaultSheet = ss.getSheetByName("Sheet1");
if (defaultSheet) ss.deleteSheet(defaultSheet);
} catch(e) {}
// Move to Administrative folder
moveFile(ss.getId(), parent);
// Build SAR procedure doc
buildSARProcedure(parent);
log("Created: Administrative folder and Client Index");
}
function buildSettingsTab(ss) {
var s = ss.insertSheet("⚙️ Settings");
s.getRange("A1").setValue("PRACTICE SETTINGS").setFontWeight("bold").setFontSize(14);
s.getRange("A2").setValue("Edit these values to configure your practice. Do not rename this tab.");
s.getRange("A2").setFontStyle("italic").setFontColor("#888888");
var settings = [
["Practice Name", CONFIG.practiceName],
["Therapist Name", CONFIG.therapistName],
["Practice Email", CONFIG.practiceEmail],
["Country", CONFIG.country],
["Default Session Fee", CONFIG.defaultFee],
["Currency Symbol", CONFIG.currency],
["Standard Session Length (mins)", CONFIG.sessionLength],
["Script Version", CONFIG.version],
["Setup Date", SETUP_DATE]
];
for (var i = 0; i < settings.length; i++) {
s.getRange(i + 4, 1).setValue(settings[i][0]).setFontWeight("bold");
s.getRange(i + 4, 2).setValue(settings[i][1]);
}
s.setColumnWidth(1, 260);
s.setColumnWidth(2, 300);
s.getRange("A4:A12").setBackground("#f0f4f8");
}
function buildClientsTab(ss) {
var s = ss.insertSheet("👥 Clients");
var headers = [
"Client Code","First Name","Last Name","Email","Phone",
"Date of Birth","Start Date","End Date","Status",
"Default Fee (" + CONFIG.currency + ")","Retention Deadline",
"Folder Link","Referral Source","Admin Notes"
];
styleHeaderRow(s, headers, "#1a3a5c");
// No example row — starts clean so Income Dashboard is accurate from day one
var widths = [100,110,110,200,140,120,110,110,100,130,140,260,140,220];
setColumnWidths(s, widths);
s.setFrozenRows(1);
// Status dropdown
applyDropdown(s, 2, 9, 200, ["Active","Closed","On Hold","Enquiry","Waitlist"]);
// Closed = grey
applyConditionalFormat(s, "Closed", "#f5f5f5", "#999999", 2, 1, 200, headers.length);
}
function buildWaitlistTab(ss) {
var s = ss.insertSheet("⏳ Waitlist");
var headers = [
"Name","Email","Phone","Date Added","Presenting Issue (brief)",
"Preferred Days/Times","Follow-up Date","Status","Notes"
];
styleHeaderRow(s, headers, "#3a5c1a");
var widths = [160,200,140,120,220,180,130,120,220];
setColumnWidths(s, widths);
s.setFrozenRows(1);
applyDropdown(s, 2, 8, 100, ["Waiting","Offered Slot","Started","Declined","Unavailable"]);
addNote(s, "A1", "Track prospective clients here before they become active. Never record clinical detail — brief presenting issue only.");
}
function buildSessionLogTab(ss) {
var s = ss.insertSheet("📅 Session_Log");
var headers = [
"Date","Client Code","Session No","Duration (mins)",
"Format","Fee (" + CONFIG.currency + ")","Paid","Payment Date",
"Invoice Ref","CPT Code (US)","Notes"
];
styleHeaderRow(s, headers, "#1a3a5c");
var widths = [120,110,100,140,130,120,80,130,130,130,220];
setColumnWidths(s, widths);
s.setFrozenRows(1);
applyDropdown(s, 2, 5, 500, ["Video — Google Meet","In Person","Phone","Group"]);
applyDropdown(s, 2, 7, 500, ["Yes","No","Waived"]);
// No example row — starts clean so Income Dashboard is accurate from day one
addNote(s, "A1", "Log every session here. CPT Code is for US therapists only — leave blank if not applicable. Common codes: 90837 (60 min), 90834 (45 min), 90832 (30 min).");
}
function buildIncomeDashboardTab(ss) {
var s = ss.insertSheet("📊 Income_Dashboard");
s.getRange("A1").setValue("INCOME DASHBOARD").setFontWeight("bold").setFontSize(16);
s.getRange("A2").setValue("Auto-calculated from Session_Log. Do not edit this sheet directly.");
s.getRange("A2").setFontStyle("italic").setFontColor("#888888");
s.getRange("A1:F1").merge().setBackground("#1a3a5c").setFontColor("#ffffff");
// ── Year selector ──
s.getRange("D4").setValue("VIEWING YEAR").setFontWeight("bold").setFontSize(11);
s.getRange("E4").setValue(new Date().getFullYear());
s.getRange("E4").setFontSize(14).setFontWeight("bold").setBackground("#fff3cd").setHorizontalAlignment("center");
s.getRange("F4").setValue("← edit this to see another year").setFontStyle("italic").setFontColor("#888888").setFontSize(9);
s.setColumnWidth(5, 120);
s.setColumnWidth(6, 220);
// Current month section
s.getRange("A4").setValue("CURRENT MONTH").setFontWeight("bold").setFontSize(12);
s.getRange("A5").setValue("Sessions completed:");
s.getRange("A6").setValue("Income earned (" + CONFIG.currency + "):");
s.getRange("A7").setValue("Outstanding payments (" + CONFIG.currency + "):");
s.getRange("A8").setValue("Sessions unpaid:");
// Formulas for current month (pull from Session_Log)
s.getRange("B5").setFormula('=COUNTIFS(\'📅 Session_Log\'!A:A,">="&DATE(YEAR(TODAY()),MONTH(TODAY()),1),\'📅 Session_Log\'!A:A,"<="&EOMONTH(TODAY(),0),\'📅 Session_Log\'!B:B,"<>Client Code")');
s.getRange("B6").setFormula('=SUMPRODUCT((MONTH(\'📅 Session_Log\'!A2:A500)=MONTH(TODAY()))*(YEAR(\'📅 Session_Log\'!A2:A500)=YEAR(TODAY()))*(\'📅 Session_Log\'!G2:G500="Yes")*(\'📅 Session_Log\'!F2:F500))');
s.getRange("B7").setFormula('=SUMPRODUCT((MONTH(\'📅 Session_Log\'!A2:A500)=MONTH(TODAY()))*(YEAR(\'📅 Session_Log\'!A2:A500)=YEAR(TODAY()))*(\'📅 Session_Log\'!G2:G500="No")*(\'📅 Session_Log\'!F2:F500))');
s.getRange("B8").setFormula('=COUNTIFS(\'📅 Session_Log\'!A:A,">="&DATE(YEAR(TODAY()),MONTH(TODAY()),1),\'📅 Session_Log\'!A:A,"<="&EOMONTH(TODAY(),0),\'📅 Session_Log\'!G:G,"No",\'📅 Session_Log\'!B:B,"<>Client Code")');
// Year summary — all formulas reference $E$4 (year selector)
s.getRange("A10").setValue("YEAR SUMMARY").setFontWeight("bold").setFontSize(12);
s.getRange("A11").setValue("Sessions completed:");
s.getRange("A12").setValue("Total income (" + CONFIG.currency + "):");
s.getRange("A13").setValue("Outstanding payments (" + CONFIG.currency + "):");
s.getRange("A14").setValue("Active clients:");
s.getRange("A15").setValue("Average fee per session (" + CONFIG.currency + "):");
s.getRange("B11").setFormula('=COUNTIFS(\'📅 Session_Log\'!A:A,">="&DATE($E$4,1,1),\'📅 Session_Log\'!A:A,"<="&DATE($E$4,12,31),\'📅 Session_Log\'!B:B,"<>Client Code")');
s.getRange("B12").setFormula('=SUMPRODUCT((YEAR(\'📅 Session_Log\'!A2:A500)=$E$4)*(\'📅 Session_Log\'!G2:G500="Yes")*(\'📅 Session_Log\'!F2:F500))');
s.getRange("B13").setFormula('=SUMPRODUCT((YEAR(\'📅 Session_Log\'!A2:A500)=$E$4)*(\'📅 Session_Log\'!G2:G500="No")*(\'📅 Session_Log\'!F2:F500))');
s.getRange("B14").setFormula('=COUNTIF(\'👥 Clients\'!I:I,"Active")');
s.getRange("B15").setFormula('=IFERROR(B12/B11,0)');
// Monthly breakdown — references $E$4
s.getRange("A17").setValue("MONTHLY BREAKDOWN").setFontWeight("bold").setFontSize(12);
s.getRange("B17").setFormula('="— "&$E$4').setFontWeight("bold").setFontSize(12);
var monthHeaders = ["Month","Sessions","Income (" + CONFIG.currency + ")","Outstanding (" + CONFIG.currency + ")","DNAs"];
for (var i = 0; i < monthHeaders.length; i++) {
s.getRange(18, i + 1).setValue(monthHeaders[i]).setFontWeight("bold").setBackground("#e8f0fe");
}
var months = ["January","February","March","April","May","June",
"July","August","September","October","November","December"];
for (var m = 0; m < 12; m++) {
var row = 19 + m;
s.getRange(row, 1).setValue(months[m]);
s.getRange(row, 2).setFormula('=COUNTIFS(\'📅 Session_Log\'!A:A,">="&DATE($E$4,' + (m+1) + ',1),\'📅 Session_Log\'!A:A,"<="&EOMONTH(DATE($E$4,' + (m+1) + ',1),0),\'📅 Session_Log\'!B:B,"<>Client Code")');
s.getRange(row, 3).setFormula('=SUMPRODUCT((MONTH(\'📅 Session_Log\'!A2:A500)=' + (m+1) + ')*(YEAR(\'📅 Session_Log\'!A2:A500)=$E$4)*(\'📅 Session_Log\'!G2:G500="Yes")*(\'📅 Session_Log\'!F2:F500))');
s.getRange(row, 4).setFormula('=SUMPRODUCT((MONTH(\'📅 Session_Log\'!A2:A500)=' + (m+1) + ')*(YEAR(\'📅 Session_Log\'!A2:A500)=$E$4)*(\'📅 Session_Log\'!G2:G500="No")*(\'📅 Session_Log\'!F2:F500))');
s.getRange(row, 5).setFormula('=COUNTIFS(\'📋 DNA_Log\'!A:A,">="&DATE($E$4,' + (m+1) + ',1),\'📋 DNA_Log\'!A:A,"<="&EOMONTH(DATE($E$4,' + (m+1) + ',1),0))');
}
s.setColumnWidth(1, 160);
s.setColumnWidth(2, 160);
s.setColumnWidth(3, 180);
s.setColumnWidth(4, 200);
s.setColumnWidth(5, 120);
// Style value cells
s.getRange("B5:B8").setBackground("#e8f4e8").setFontWeight("bold");
s.getRange("B11:B15").setBackground("#e8f4e8").setFontWeight("bold");
}
function buildSupervisionLogTab(ss) {
var s = ss.insertSheet("🎓 Supervision_Log");
var headers = [
"Date","Supervisor Name","Duration (hrs)","Format",
"Cases Discussed (codes only)","Key Themes","Actions Agreed","Signature/Confirm"
];
styleHeaderRow(s, headers, "#3a1a5c");
var widths = [120,180,120,130,220,220,220,160];
setColumnWidths(s, widths);
s.setFrozenRows(1);
applyDropdown(s, 2, 4, 200, ["In Person","Video","Phone","Group"]);
addNote(s, "A1", "Record all supervision here. Use client codes only — never client names. Required by BACP, IACP and most professional bodies. Keep this log as evidence of ongoing supervision for registration renewal.");
}
function buildCPDLogTab(ss) {
var s = ss.insertSheet("📚 CPD_Log");
var headers = [
"Date","Activity Title","Provider","Type",
"Hours","Notes","Certificate/Evidence"
];
styleHeaderRow(s, headers, "#5c3a1a");
var widths = [120,240,180,160,80,240,200];
setColumnWidths(s, widths);
s.setFrozenRows(1);
applyDropdown(s, 2, 4, 200, ["Training Course","Workshop","Webinar","Reading","Personal Therapy","Supervision","Peer Consultation","Conference","Other"]);
// CPD total
s.getRange("I1").setValue("Total CPD Hours (this year):");
s.getRange("J1").setFormula('=SUMPRODUCT((YEAR(A2:A500)=YEAR(TODAY()))*(E2:E500))');
s.getRange("I1").setFontWeight("bold");
s.getRange("J1").setBackground("#e8f4e8").setFontWeight("bold");
addNote(s, "A1", "Log all CPD activity here. BACP requires 30 hours per year. IACP requires 30 hours. The total at top right calculates automatically for the current year.");
}
function buildDNALogTab(ss) {
var s = ss.insertSheet("📋 DNA_Log");
var headers = [
"Date","Client Code","Session No","Contact Attempted",
"Client Response","Action Taken","Safeguarding Concern","Notes"
];
styleHeaderRow(s, headers, "#5c1a1a");
var widths = [120,110,100,160,160,200,160,220];
setColumnWidths(s, widths);
s.setFrozenRows(1);
applyDropdown(s, 2, 4, 200, ["Yes — Email","Yes — Phone","Yes — Both","No contact attempted"]);
applyDropdown(s, 2, 7, 200, ["No","Yes — noted","Yes — escalated"]);
addNote(s, "A1", "Log every Did Not Attend (DNA) and late cancellation here. Important for safeguarding — if a vulnerable client repeatedly disappears, this record supports any duty of care action taken.");
}
function buildDeletionLogTab(ss) {
var s = ss.insertSheet("🗑️ Deletion_Log");
var headers = [
"Client Code","Retention Deadline","Deletion Date","Deleted By","Method","Confirmation"
];
styleHeaderRow(s, headers, "#3a3a3a");
var widths = [120,160,140,160,200,280];
setColumnWidths(s, widths);
s.setFrozenRows(1);
applyDropdown(s, 2, 5, 200, ["Google Drive permanent delete + Trash emptied","Manual deletion confirmed"]);
addNote(s, "A1", "Log every client record deletion here. This log must be kept permanently — even after the client records themselves are deleted. It is your audit trail for GDPR compliance.");
}
function buildSARLogTab(ss) {
var s = ss.insertSheet("📨 SAR_Log");
var headers = [
"Client Code","Request Date","Response Due (30 days)","Response Sent","Method","Notes"
];
styleHeaderRow(s, headers, "#1a3a5c");
var widths = [120,140,180,140,160,280];
setColumnWidths(s, widths);
s.setFrozenRows(1);
applyDropdown(s, 2, 5, 200, ["Workspace email","Secure folder share","Post"]);
addNote(s, "A1", "Log all Subject Access Requests here. You have 30 days to respond. See SAR Procedure document in 2_Administrative for step-by-step instructions.");
}
function buildHowToUseTab(ss) {
var s = ss.insertSheet("ℹ️ How To Use");
s.getRange("A1").setValue("CLIENT INDEX — HOW TO USE").setFontWeight("bold").setFontSize(14);
var content = [
[""],
["⚙️ SETTINGS TAB"],
["Edit your practice details here. These values are used throughout the system."],
[""],
["👥 CLIENTS TAB"],
["One row per client. Assign codes sequentially: C001, C002, C003 etc."],
["Never reuse a code, even after a client has been deleted."],
["Default Fee auto-populates session log when you add a new session via Practice Tools."],
["Retention Deadline = End Date + 7 years. Add when closing a case."],
["Paste the Google Drive folder link in Folder Link for one-click access."],
[""],
["⏳ WAITLIST TAB"],
["Track prospective clients before they start. Brief presenting issue only — no clinical detail."],
[""],
["📅 SESSION_LOG TAB"],
["Log every session here. Use Practice Tools → Log Session for a guided form."],
["CPT Code is for US therapists only. Common codes: 90837 (60 min), 90834 (45 min), 90832 (30 min)."],
["Fee auto-fills from the client's Default Fee in the Clients tab."],
[""],
["📊 INCOME_DASHBOARD TAB"],
["Auto-calculated from Session_Log. Never edit this tab directly."],
["Shows current month totals and year-to-date breakdown."],
[""],
["🎓 SUPERVISION_LOG TAB"],
["Record all supervision. Use client codes only — never client names."],
["Required by BACP (1.5 hrs/month min), IACP and most professional bodies."],
[""],
["📚 CPD_LOG TAB"],
["Log all CPD activity. Total hours for current year calculated automatically."],
["BACP and IACP require 30 hours per year."],
[""],
["📋 DNA_LOG TAB"],
["Log every Did Not Attend and late cancellation."],
["Flag safeguarding concerns — important if a vulnerable client stops responding."],
[""],
["🗑️ DELETION_LOG TAB"],
["Log every permanent deletion of client records. Keep this log permanently."],
[""],
["📨 SAR_LOG TAB"],
["Log all Subject Access Requests. 30-day response deadline."],
[""],
["IMPORTANT"],
["This spreadsheet contains personal data. Keep it private."],
["Do not share with anyone not authorised to see client information."],
];
for (var i = 0; i < content.length; i++) {
s.getRange(i + 2, 1).setValue(content[i][0]);
if (content[i][0] && content[i][0].match(/^[⚙📅📊🎓📚📋🗑📨👥⏳ℹ]/)) {
s.getRange(i + 2, 1).setFontWeight("bold").setFontSize(11);
}
}
s.setColumnWidth(1, 650);
}
function buildSARProcedure(parent) {
var doc = DocumentApp.create("Procedure — Subject Access Requests");
var b = doc.getBody();
h1(b, "Subject Access Request (SAR) Procedure");
b.appendParagraph("Practice: " + CONFIG.practiceName + " | Last reviewed: " + SETUP_DATE);
b.appendParagraph("");
h2(b, "What is a Subject Access Request?");
b.appendParagraph("Under GDPR (UK/Ireland) and HIPAA (US), clients have the right to request a copy of all personal data you hold about them. You must respond within 30 days at no charge.");
b.appendParagraph("");
h2(b, "Step-by-Step Response");
b.appendParagraph("1. Verify identity — confirm name, date of birth and contact email before releasing any data.");
b.appendParagraph("2. Log the request — open Client_Index → SAR_Log tab. Add client code, request date and 30-day deadline.");
b.appendParagraph("3. Gather the data — go to their folder in 1_Clinical_Records. Collect all documents containing their personal data.");
b.appendParagraph("4. Send via Workspace email — compile into PDF or secure folder share. Never use personal Gmail or WhatsApp.");
b.appendParagraph("5. Log the response — update SAR_Log with the date sent.");
b.appendParagraph("");
h2(b, "What You Are Not Required to Provide");
b.appendParagraph("• Personal reflective or supervision notes not held in the client file");
b.appendParagraph("• Data about third parties mentioned in notes");
b.appendParagraph("• Information where disclosure would prejudice legal proceedings");
b.appendParagraph("");
b.appendParagraph("If in doubt, consult your professional body or a GDPR adviser before responding.");
doc.saveAndClose();
moveFile(doc.getId(), parent);
}
// ============================================================
// 3. FINANCE
// ============================================================
function buildFinance(parent) {
createFolder(parent, "Invoices");
createFolder(parent, "Receipts");
createFolder(parent, "Tax_Documents");
createFolder(parent, "Stripe_Summaries");
createFolder(parent, "Superbills");
var doc = DocumentApp.create("📋 Finance — Quick Reference");
var b = doc.getBody();
h1(b, "Finance Folder — Quick Reference");
bold(b, "Folder contents:");
b.appendParagraph("Invoices — client invoices. Name: YYYY-MM-DD_C001_Invoice");
b.appendParagraph("Receipts — business expense receipts");
b.appendParagraph("Tax_Documents — annual returns, accountant correspondence");
b.appendParagraph("Stripe_Summaries — monthly Stripe payout summaries");
b.appendParagraph("Superbills — US superbills generated for clients");
b.appendParagraph("");
bold(b, "Accountant access:");
b.appendParagraph("Share this Finance folder only. Right-click → Share → Viewer. Never share Clinical Records.");
b.appendParagraph("");
bold(b, "Invoice content:");
b.appendParagraph("Reference client codes only — never diagnoses or session content.");
b.appendParagraph("Example: 'Therapy session — C001 — 50 minutes'");
doc.saveAndClose();
moveFile(doc.getId(), parent);
log("Created: Finance folder");
}
// ============================================================
// 4. TEMPLATES & POLICIES
// ============================================================
function buildTemplates(parent) {
var consentFolder = createFolder(parent, "Consent_Templates");
var intakeFolder = createFolder(parent, "Intake_Templates");
var noteFolder = createFolder(parent, "Session_Note_Templates");
var billingFolder = createFolder(parent, "Billing_Templates");
var clinicalFolder = createFolder(parent, "Clinical_Templates");
var policyFolder = createFolder(parent, "Policies");
buildConsentForm(consentFolder);
buildPrivacyNotice(consentFolder);
buildIntakeForm(intakeFolder);
buildSessionNoteTemplate(noteFolder);
buildSuperbill(billingFolder);
buildInsuranceReceipt(billingFolder);
buildReferralLetter(clinicalFolder);
buildEndingSummary(clinicalFolder);
buildRiskRegister(clinicalFolder);
buildDataRetentionPolicy(policyFolder);
buildTelehealthPolicy(policyFolder);
buildAnnualComplianceChecklist(policyFolder);
log("Created: All templates and policies");
}
function buildConsentForm(folder) {
var doc = DocumentApp.create("TEMPLATE — Therapy Consent Form");
var b = doc.getBody();
var w1 = b.appendParagraph("⚠️ TEMPLATE — Fill in all [BRACKETED] fields before sending to each client. Save signed copy to client's 02_Consent_Forms folder.");
w1.setItalic(true); setRed(w1);
b.appendParagraph("");
h1(b, "Therapy Consent Form");
b.appendParagraph("Practice: " + CONFIG.practiceName + " | Therapist: " + CONFIG.therapistName);
b.appendParagraph("Date: ___________ | Client Code: ___________ (therapist use only)");
b.appendParagraph("");
var pi = b.appendParagraph("Please read carefully before signing."); pi.setItalic(true);
b.appendParagraph("");
h2(b, "1. Confidentiality");
b.appendParagraph("Everything discussed in sessions is confidential. I will not share information about you without your consent, except:");
b.appendParagraph("• Where there is serious risk of harm to you or another person");
b.appendParagraph("• Where required by law or court order");
b.appendParagraph("• In clinical supervision — discussed anonymously with a qualified supervisor");
b.appendParagraph("");
h2(b, "2. Sessions");
b.appendParagraph("Sessions are [LENGTH] minutes, held [FREQUENCY] via Google Meet. Please be in a private, confidential space.");
b.appendParagraph("");
h2(b, "3. Cancellation");
b.appendParagraph("Please give [X hours/days] notice to cancel or reschedule. Late cancellations are charged at the full rate.");
b.appendParagraph("");
h2(b, "4. Payment");
b.appendParagraph("Fee: [" + CONFIG.currency + " AMOUNT] per session. Payment collected in advance via Stripe.");
b.appendParagraph("");
h2(b, "5. Between Sessions");
b.appendParagraph("Contact via email at " + CONFIG.practiceEmail + " only. I do not provide therapeutic support between sessions.");
b.appendParagraph("In crisis: contact your GP, call 999 (UK/Ireland) or 911 (US), or Samaritans: 116 123.");
b.appendParagraph("");
h2(b, "6. Your Data");
b.appendParagraph("Records are stored securely in Google Workspace. You may request a copy of all data held about you at any time. See my Privacy Notice for full details.");
b.appendParagraph("");
h2(b, "7. Professional Registration");
b.appendParagraph("I am registered with [PROFESSIONAL BODY] and hold professional indemnity insurance. Concerns may be referred to [PROFESSIONAL BODY].");
b.appendParagraph("");
h2(b, "Agreement");
b.appendParagraph("I confirm I have read and understood this consent form and agree to its terms.");
b.appendParagraph("");
b.appendParagraph("Name (print): ___________________________________________");
b.appendParagraph("");
b.appendParagraph("Signature: ___________________________________________");
b.appendParagraph("");
b.appendParagraph("Date: ___________________________________________");
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildPrivacyNotice(folder) {
var doc = DocumentApp.create("TEMPLATE — Privacy Notice");
var b = doc.getBody();
var wp = b.appendParagraph("⚠️ TEMPLATE — Review annually. Send to each new client alongside consent form."); wp.setItalic(true); setRed(wp);
b.appendParagraph("");
h1(b, "Privacy Notice");
b.appendParagraph("Practice: " + CONFIG.practiceName + " | Data Controller: " + CONFIG.therapistName);
b.appendParagraph("Contact: " + CONFIG.practiceEmail + " | Last reviewed: " + SETUP_DATE);
b.appendParagraph("");
h2(b, "1. Who I Am");
b.appendParagraph(CONFIG.therapistName + " is the data controller for " + CONFIG.practiceName + " and is responsible for how your personal data is used.");
b.appendParagraph("");
h2(b, "2. What Data I Collect");
b.appendParagraph("• Identity — name, date of birth, emergency contact");
b.appendParagraph("• Contact — email, phone number");
b.appendParagraph("• Health data — mental and physical health information (special category under GDPR)");
b.appendParagraph("• Financial — payment records via Stripe");
b.appendParagraph("• Appointment — booking records via Cal.com");
b.appendParagraph("");
h2(b, "3. Legal Basis");
b.appendParagraph("Processing of health data is under Article 9(2)(h) GDPR — necessary for health or social care. Financial data processed for contract performance.");
b.appendParagraph("");
h2(b, "4. Where Data Is Stored");
b.appendParagraph("• Google Workspace — clinical records, email, video sessions (Google DPA in place)");
b.appendParagraph("• Cal.com — booking (name and email only)");
b.appendParagraph("• Stripe — payments (financial data only)");
b.appendParagraph("• Framer — public website (no client data stored)");
b.appendParagraph("");
h2(b, "5. How Long I Keep Your Data");
b.appendParagraph("Adult clients: 7 years after end of therapy. Minor clients: until age 25 or 7 years, whichever is later.");
b.appendParagraph("");
h2(b, "6. Your Rights");
b.appendParagraph("You have the right to access, correct or request deletion of your data, and to withdraw consent at any time.");
b.appendParagraph("Contact: " + CONFIG.practiceEmail);
b.appendParagraph("Supervisory authorities: UK: ico.org.uk | Ireland: dataprotection.ie | US: hhs.gov/hipaa");
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildIntakeForm(folder) {
var doc = DocumentApp.create("TEMPLATE — Client Intake Form");
var b = doc.getBody();
var wp = b.appendParagraph("⚠️ TEMPLATE — This is a suggested starting point. Adapt it to your practice. Use in person, send as a PDF, or complete verbally. Save completed forms to client's 03_Intake_Forms folder."); wp.setItalic(true); setRed(wp);
b.appendParagraph("");
h1(b, "Client Intake Form");
b.appendParagraph("Practice: " + CONFIG.practiceName + " | Therapist: " + CONFIG.therapistName);
b.appendParagraph("Date: ___________ | Client Code: ___________ (therapist use)");
b.appendParagraph("");
var pi = b.appendParagraph("All information is confidential and stored securely in line with our Privacy Notice."); pi.setItalic(true);
b.appendParagraph("");
h2(b, "Personal Details");
b.appendParagraph("Full name: ___________________________________________");
b.appendParagraph("Date of birth: ___________________________________________");
b.appendParagraph("Email: ___________________________________________");
b.appendParagraph("Phone: ___________________________________________");
b.appendParagraph("Emergency contact (name and number): ___________________________________________");
b.appendParagraph("");
h2(b, "GP Details");
b.appendParagraph("GP name: ___________________________________________");
b.appendParagraph("GP surgery: ___________________________________________");
b.appendParagraph("Under psychiatrist or mental health service? ☐ Yes ☐ No");
b.appendParagraph("If yes: ___________________________________________");
b.appendParagraph("");
h2(b, "Current Situation");
b.appendParagraph("What brings you to therapy now?");
b.appendParagraph(" ");
b.appendParagraph(" ");
b.appendParagraph("How long have you been experiencing this? ___________________________________________");
b.appendParagraph("Previous therapy? ☐ Yes ☐ No If yes: ___________________________________________");
b.appendParagraph("");
h2(b, "Health Information");
b.appendParagraph("Current medication? ☐ Yes ☐ No If yes: ___________________________________________");
b.appendParagraph("Relevant physical health conditions? ☐ Yes ☐ No If yes: ___________________________________________");
b.appendParagraph("Thoughts of self-harm or suicide? ☐ Yes ☐ No Currently? ☐ Yes ☐ No");
b.appendParagraph("Context: ___________________________________________");
b.appendParagraph("");
h2(b, "What You're Looking For");
b.appendParagraph("What would you like to get from therapy?");
b.appendParagraph(" ");
b.appendParagraph(" ");
b.appendParagraph("Anything else you'd like me to know?");
b.appendParagraph(" ");
b.appendParagraph("");
h2(b, "Declaration");
b.appendParagraph("I confirm this information is accurate and consent to it being stored securely for therapy purposes.");
b.appendParagraph("");
b.appendParagraph("Signature: ___________________________________________ Date: ___________");
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildSessionNoteTemplate(folder) {
var doc = DocumentApp.create("TEMPLATE — Session Note");
var b = doc.getBody();
var wp = b.appendParagraph("⚠️ TEMPLATE — Make a copy. Never write in this file. Save copy to client's 04_Session_Notes folder as YYYY-MM-DD_C001_SessionNote."); wp.setItalic(true); setRed(wp);
b.appendParagraph("");
h1(b, "Session Note");
b.appendParagraph("Client Code: ___________ Session No: ___________");
b.appendParagraph("Date: ___________ Duration: ___________");
b.appendParagraph("Format: ☐ Video — Google Meet ☐ In Person ☐ Phone");
b.appendParagraph("");
h2(b, "Presenting themes this session");
b.appendParagraph(" "); b.appendParagraph(" ");
h2(b, "Interventions used");
b.appendParagraph(" "); b.appendParagraph(" ");
h2(b, "Client response and engagement");
b.appendParagraph(" "); b.appendParagraph(" ");
h2(b, "Risk assessment");
b.appendParagraph("Risk to self: ☐ None identified ☐ Low ☐ Medium ☐ High");
b.appendParagraph("Risk to others: ☐ None identified ☐ Low ☐ Medium ☐ High");
b.appendParagraph("Action taken: ___________________________________________");
b.appendParagraph("");
h2(b, "Plan for next session");
b.appendParagraph(" "); b.appendParagraph(" ");
h2(b, "Supervision");
b.appendParagraph("Bring to supervision: ☐ Yes ☐ No");
b.appendParagraph("Notes: ___________________________________________");
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildSuperbill(folder) {
var doc = DocumentApp.create("TEMPLATE — Superbill (US)");
var b = doc.getBody();
var wp = b.appendParagraph("⚠️ US THERAPISTS ONLY — Make a copy per client per billing period. Fill in all fields. Diagnosis code must be assigned by a licensed clinician only."); wp.setItalic(true); setRed(wp);
b.appendParagraph("");
h1(b, "Superbill");
b.appendParagraph("Statement Date: ___________ | Statement No: ___________");
b.appendParagraph("");
h2(b, "Provider Information");
b.appendParagraph("Provider Name: " + CONFIG.therapistName);
b.appendParagraph("Practice Name: " + CONFIG.practiceName);
b.appendParagraph("Credentials: ___________ (e.g. LCSW, LPC, PhD, PsyD)");
b.appendParagraph("NPI Number: ___________ (required for insurance submission)");
b.appendParagraph("Tax ID / EIN: ___________");
b.appendParagraph("Address: ___________________________________________");
b.appendParagraph("Phone: ___________________________________________");
b.appendParagraph("Email: " + CONFIG.practiceEmail);
b.appendParagraph("");
h2(b, "Client Information");
b.appendParagraph("Client Name: ___________________________________________");
b.appendParagraph("Date of Birth: ___________________________________________");
b.appendParagraph("Client ID / Code: ___________________________________________");
b.appendParagraph("Insurance Provider: ___________________________________________");
b.appendParagraph("Insurance Member ID: ___________________________________________");
b.appendParagraph("");
h2(b, "Diagnosis");
b.appendParagraph("Primary Diagnosis (ICD-10): ___________ — ___________________________________________");
b.appendParagraph("Secondary Diagnosis (if applicable): ___________ — ___________________________________________");
b.appendParagraph("");
var pi = b.appendParagraph("⚠️ Diagnosis codes must be assigned by a licensed clinician. Only include a diagnosis code if you are licensed and qualified to diagnose in your jurisdiction."); pi.setItalic(true);
b.appendParagraph("");
h2(b, "Services Rendered");
// Table header row as text (Apps Script tables are complex — plain text is cleaner)
b.appendParagraph("Date of Service | CPT Code | Service Description | Duration | Fee | Amount Paid");
b.appendParagraph("─────────────────────────────────────────────────────────────────────────────");
b.appendParagraph("___________ | _______ | _________________________________ | _____ min | " + CONFIG.currency + "_______ | " + CONFIG.currency + "_______");
b.appendParagraph("___________ | _______ | _________________________________ | _____ min | " + CONFIG.currency + "_______ | " + CONFIG.currency + "_______");
b.appendParagraph("___________ | _______ | _________________________________ | _____ min | " + CONFIG.currency + "_______ | " + CONFIG.currency + "_______");
b.appendParagraph("___________ | _______ | _________________________________ | _____ min | " + CONFIG.currency + "_______ | " + CONFIG.currency + "_______");
b.appendParagraph("─────────────────────────────────────────────────────────────────────────────");
b.appendParagraph("TOTAL CHARGED: " + CONFIG.currency + "___________ | TOTAL PAID: " + CONFIG.currency + "___________ | BALANCE: " + CONFIG.currency + "___________");
b.appendParagraph("");
h2(b, "Common CPT Codes (Psychotherapy)");
b.appendParagraph("90837 — Individual psychotherapy, 60 minutes");
b.appendParagraph("90834 — Individual psychotherapy, 45 minutes");
b.appendParagraph("90832 — Individual psychotherapy, 30 minutes");
b.appendParagraph("90847 — Family psychotherapy with client present");
b.appendParagraph("90846 — Family psychotherapy without client present");
b.appendParagraph("90853 — Group psychotherapy");
b.appendParagraph("99213 — Office visit, established patient, moderate complexity");
b.appendParagraph("");
h2(b, "Place of Service Codes");
b.appendParagraph("02 — Telehealth (patient is not at home)");
b.appendParagraph("10 — Telehealth (patient is at home)");
b.appendParagraph("11 — Office");
b.appendParagraph("");
h2(b, "Provider Signature");
b.appendParagraph("I certify that the services listed above were medically necessary and personally rendered by me.");
b.appendParagraph("");
b.appendParagraph("Signature: ___________________________________________ Date: ___________");
b.appendParagraph("");
var pi = b.appendParagraph("📝 Save to: 3_Finance → Superbills → YYYY-MM_C001_Superbill"); pi.setItalic(true);
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildInsuranceReceipt(folder) {
var doc = DocumentApp.create("TEMPLATE — Insurance Receipt (UK and Ireland)");
var b = doc.getBody();
var wp = b.appendParagraph("⚠️ UK AND IRELAND — Make a copy per client per billing period. For clients submitting to private health insurers (Bupa, AXA, VHI, Laya etc)."); wp.setItalic(true); setRed(wp);
b.appendParagraph("");
h1(b, "Therapy Receipt for Insurance Reimbursement");
b.appendParagraph("Receipt Date: ___________ | Receipt No: ___________");
b.appendParagraph("");
h2(b, "Therapist Details");
b.appendParagraph("Name: " + CONFIG.therapistName);
b.appendParagraph("Practice: " + CONFIG.practiceName);
b.appendParagraph("Qualifications: ___________ (e.g. MSc Counselling Psychology, MBACP)");
b.appendParagraph("Professional Registration: ___________ (e.g. BACP No. 123456)");
b.appendParagraph("Email: " + CONFIG.practiceEmail);
b.appendParagraph("");
h2(b, "Client Details");
b.appendParagraph("Client Name: ___________________________________________");
b.appendParagraph("Date of Birth: ___________________________________________");
b.appendParagraph("Insurance Provider: ___________________________________________");
b.appendParagraph("Policy Number: ___________________________________________");
b.appendParagraph("Authorisation Number (if applicable): ___________________________________________");
b.appendParagraph("");
h2(b, "Sessions");
b.appendParagraph("Date | Duration | Service | Fee | Paid");
b.appendParagraph("─────────────────────────────────────────────────");
b.appendParagraph("___________ | _____ min | Individual Therapy | " + CONFIG.currency + "_______ | " + CONFIG.currency + "_______");
b.appendParagraph("___________ | _____ min | Individual Therapy | " + CONFIG.currency + "_______ | " + CONFIG.currency + "_______");
b.appendParagraph("___________ | _____ min | Individual Therapy | " + CONFIG.currency + "_______ | " + CONFIG.currency + "_______");
b.appendParagraph("─────────────────────────────────────────────────");
b.appendParagraph("TOTAL PAID: " + CONFIG.currency + "___________");
b.appendParagraph("");
var pi = b.appendParagraph("Note: Diagnosis codes are not routinely included on therapy receipts in the UK and Ireland. If your insurer requires a clinical diagnosis, this must be obtained from a psychiatrist or GP, not from your therapist."); pi.setItalic(true);
b.appendParagraph("");
h2(b, "Declaration");
b.appendParagraph("I confirm that the therapy sessions listed above were provided by me.");
b.appendParagraph("");
b.appendParagraph("Signature: ___________________________________________ Date: ___________");
b.appendParagraph("");
var pi = b.appendParagraph("📝 Save to: 3_Finance → Invoices → YYYY-MM_C001_InsuranceReceipt"); pi.setItalic(true);
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildReferralLetter(folder) {
var doc = DocumentApp.create("TEMPLATE — Referral Letter");
var b = doc.getBody();
var wp = b.appendParagraph("⚠️ TEMPLATE — Make a copy. Fill in all [BRACKETED] fields. Save copy to client's 06_Correspondence folder."); wp.setItalic(true); setRed(wp);
b.appendParagraph("");
b.appendParagraph(CONFIG.practiceName);
b.appendParagraph(CONFIG.practiceEmail);
b.appendParagraph("[DATE]");
b.appendParagraph("");
b.appendParagraph("Dear [RECIPIENT NAME / Dr / To Whom It May Concern],");
b.appendParagraph("");
b.appendParagraph("Re: [CLIENT — use initials only unless client has consented to full name being shared]");
b.appendParagraph("Date of Birth: [DOB]");
b.appendParagraph("");
b.appendParagraph("I am writing to refer the above-named client, who has been receiving [individual / couples / family] therapy with me since [START DATE].");
b.appendParagraph("");
b.appendParagraph("Presenting concerns:");
b.appendParagraph("[Brief, professional summary of presenting issues]");
b.appendParagraph("");
b.appendParagraph("Reason for referral:");
b.appendParagraph("[Why you are referring — e.g. client requires assessment, medication review, higher level of care, specialist support]");
b.appendParagraph("");
b.appendParagraph("Current risk level: [Low / Medium / High — and brief rationale]");
b.appendParagraph("");
b.appendParagraph("I am happy to discuss this referral further if helpful.");
b.appendParagraph("");
b.appendParagraph("Yours sincerely,");
b.appendParagraph("");
b.appendParagraph(CONFIG.therapistName);
b.appendParagraph("[Qualifications]");
b.appendParagraph("[Professional Body Registration]");
b.appendParagraph(CONFIG.practiceEmail);
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildEndingSummary(folder) {
var doc = DocumentApp.create("TEMPLATE — Therapy Ending Summary");
var b = doc.getBody();
var wp = b.appendParagraph("⚠️ TEMPLATE — Complete when therapy ends. Save to client's 01_Admin folder. Send to GP if clinically appropriate and client has consented."); wp.setItalic(true); setRed(wp);
b.appendParagraph("");
h1(b, "Therapy Ending Summary");
b.appendParagraph("Client Code: ___________ | Date of Final Session: ___________");
b.appendParagraph("Therapist: " + CONFIG.therapistName + " | Practice: " + CONFIG.practiceName);
b.appendParagraph("Total Sessions: ___________ | Duration of Work: ___________");
b.appendParagraph("");
h2(b, "1. Presenting Issues at Start of Therapy");
b.appendParagraph(" "); b.appendParagraph(" ");
h2(b, "2. Therapeutic Approach Used");
b.appendParagraph(" "); b.appendParagraph(" ");
h2(b, "3. Progress and Outcomes");
b.appendParagraph(" "); b.appendParagraph(" ");
h2(b, "4. Risk at Ending");
b.appendParagraph("Risk to self: ☐ None ☐ Low ☐ Medium ☐ High");
b.appendParagraph("Risk to others: ☐ None ☐ Low ☐ Medium ☐ High");
b.appendParagraph("Notes: ___________________________________________");
b.appendParagraph("");
h2(b, "5. Reason for Ending");
b.appendParagraph("☐ Planned ending — goals achieved");
b.appendParagraph("☐ Planned ending — natural conclusion");
b.appendParagraph("☐ Client-initiated ending");
b.appendParagraph("☐ Therapist-initiated ending");
b.appendParagraph("☐ Did not attend — case closed after [X] missed sessions");
b.appendParagraph("☐ Referred on to: ___________________________________________");
b.appendParagraph("");
h2(b, "6. Recommendations");
b.appendParagraph(" "); b.appendParagraph(" ");
h2(b, "7. Client Feedback (if provided)");
b.appendParagraph(" "); b.appendParagraph(" ");
b.appendParagraph("");
b.appendParagraph("Therapist signature: ___________________________________________ Date: ___________");
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildRiskRegister(folder) {
var doc = DocumentApp.create("TEMPLATE — Risk Register");
var b = doc.getBody();
var wp = b.appendParagraph("⚠️ TEMPLATE — Make a copy for any client where ongoing risk is identified. Save to client's 01_Admin folder. Review at every session where risk is present."); wp.setItalic(true); setRed(wp);
b.appendParagraph("");
h1(b, "Risk Register");
b.appendParagraph("Client Code: ___________ | Date opened: ___________");
b.appendParagraph("Therapist: " + CONFIG.therapistName);
b.appendParagraph("");
h2(b, "Risk Summary");
b.appendParagraph("Nature of risk: ___________________________________________");
b.appendParagraph("Initial risk level: ☐ Low ☐ Medium ☐ High");
b.appendParagraph("Protective factors: ___________________________________________");
b.appendParagraph("");
h2(b, "Risk Review Log");
b.appendParagraph("Date | Risk Level | Notes | Action Taken | Next Review");
b.appendParagraph("─────────────────────────────────────────────────────────────────");
for (var i = 0; i < 10; i++) {
b.appendParagraph("___________ | ☐ Low ☐ Med ☐ High | _______________ | _______________ | ___________");
}
b.appendParagraph("");
h2(b, "Escalation Record");
b.appendParagraph("Date | Action | Person Contacted | Outcome");
b.appendParagraph("─────────────────────────────────────────────────────────────────");
b.appendParagraph("___________ | _________________________________ | _________________ | _________________");
b.appendParagraph("___________ | _________________________________ | _________________ | _________________");
b.appendParagraph("");
var pi = b.appendParagraph("Bring to supervision whenever risk level is Medium or above."); pi.setItalic(true);
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildDataRetentionPolicy(folder) {
var doc = DocumentApp.create("Policy — Data Retention");
var b = doc.getBody();
h1(b, "Data Retention Policy");
b.appendParagraph("Practice: " + CONFIG.practiceName + " | Controller: " + CONFIG.therapistName);
b.appendParagraph("Last reviewed: " + SETUP_DATE);
b.appendParagraph("");
h2(b, "1. Purpose");
b.appendParagraph("This policy sets out how " + CONFIG.practiceName + " retains and deletes client data in compliance with GDPR (UK/Ireland) and HIPAA (US).");
b.appendParagraph("");
h2(b, "2. Retention Periods");
b.appendParagraph("Adult clients: 7 years after final session.");
b.appendParagraph("Minor clients: Until age 25 or 7 years after final session, whichever is later.");
b.appendParagraph("Financial records: 7 years for tax purposes.");
b.appendParagraph("");
h2(b, "3. Storage");
b.appendParagraph("All records in Google Workspace under Google's Data Processing Agreement. Two-factor authentication enforced.");
b.appendParagraph("");
h2(b, "4. Deletion Procedure");
b.appendParagraph("1. Permanently delete client folder from 5_Archives");
b.appendParagraph("2. Empty Google Drive Trash");
b.appendParagraph("3. Log in Client_Index → Deletion_Log tab");
b.appendParagraph("4. Update status to Deleted in Clients tab");
b.appendParagraph("");
h2(b, "5. Data Processors");
b.appendParagraph("• Google Workspace — email, Drive, Meet, Forms");
b.appendParagraph("• Cal.com — appointment scheduling");
b.appendParagraph("• Stripe — payment processing");
b.appendParagraph("• Framer — public website (no client data)");
b.appendParagraph("");
h2(b, "6. Breach Notification");
b.appendParagraph("Notify supervisory authority within 72 hours:");
b.appendParagraph("UK: ico.org.uk | Ireland: dataprotection.ie | US: hhs.gov/hipaa");
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildTelehealthPolicy(folder) {
var doc = DocumentApp.create("Policy — Telehealth");
var b = doc.getBody();
h1(b, "Telehealth Policy");
b.appendParagraph("Practice: " + CONFIG.practiceName + " | Last reviewed: " + SETUP_DATE);
b.appendParagraph("");
h2(b, "Platform");
b.appendParagraph("All remote sessions via Google Meet. Sessions are not recorded. Encrypted in transit.");
b.appendParagraph("");
h2(b, "Client Responsibilities");
b.appendParagraph("• Be in a private, confidential space");
b.appendParagraph("• Use a secure, private internet connection");
b.appendParagraph("• Do not record without prior written consent");
b.appendParagraph("• Confirm your physical location at the start of each session");
b.appendParagraph("");
h2(b, "Suitability");
b.appendParagraph("Telehealth is not suitable for all presentations. If remote sessions become clinically inappropriate, an alternative will be agreed.");
b.appendParagraph("");
h2(b, "Crisis Protocol");
b.appendParagraph("If a client is in crisis, the therapist will assess risk and contact emergency services if there is immediate risk to life (999 UK/Ireland, 911 US). Physical location is confirmed at session start for this reason.");
b.appendParagraph("");
h2(b, "Between-Session Communication");
b.appendParagraph("Workspace email only. No therapeutic support via WhatsApp, SMS or social media.");
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
function buildAnnualComplianceChecklist(folder) {
var doc = DocumentApp.create("Annual Compliance Review Checklist");
var b = doc.getBody();
h1(b, "Annual Compliance Review Checklist");
b.appendParagraph("Practice: " + CONFIG.practiceName + " | Therapist: " + CONFIG.therapistName);
b.appendParagraph("Review date: ___________");
b.appendParagraph("");
var pi = b.appendParagraph("Complete once per year. Keep a signed copy here in Policies."); pi.setItalic(true);
b.appendParagraph("");
h2(b, "Google Workspace Security");
b.appendParagraph("☐ Two-factor authentication enabled");
b.appendParagraph("☐ No unauthorised devices have access");
b.appendParagraph("☐ Sharing set to restricted — no public links active");
b.appendParagraph("☐ Password changed in the last 12 months");
b.appendParagraph("");
h2(b, "Client Records");
b.appendParagraph("☐ All active clients have a folder in 1_Clinical_Records");
b.appendParagraph("☐ All closed clients moved to 5_Archives");
b.appendParagraph("☐ Client Index is current and accurate");
b.appendParagraph("☐ Retention deadlines recorded for all closed clients");
b.appendParagraph("☐ Clients past retention deadline deleted and logged");
b.appendParagraph("");
h2(b, "Policies");
b.appendParagraph("☐ Privacy Notice reviewed and current");
b.appendParagraph("☐ Consent form reviewed and current");
b.appendParagraph("☐ Data Retention Policy reviewed");
b.appendParagraph("☐ Telehealth Policy reviewed");
b.appendParagraph("☐ Processor list accurate (Google, Cal.com, Stripe, Framer)");
b.appendParagraph("");
h2(b, "Third Party Tools");
b.appendParagraph("☐ Google Workspace DPA in place (admin.google.com → Account → Legal)");
b.appendParagraph("☐ Cal.com — name and email only, no clinical data");
b.appendParagraph("☐ Stripe — financial data only, no clinical content");
b.appendParagraph("☐ No new tools introduced storing client data without a DPA");
b.appendParagraph("");
h2(b, "Professional");
b.appendParagraph("☐ Professional indemnity insurance current");
b.appendParagraph("☐ Professional body registration current");
b.appendParagraph("☐ Supervision in place and up to date");
b.appendParagraph("☐ CPD hours on track (30 hours/year — check CPD_Log tab)");
b.appendParagraph("");
b.appendParagraph("Signature: ___________________________________________ Date: ___________");
doc.saveAndClose();
moveFile(doc.getId(), folder);
}
// ============================================================
// 5. ARCHIVES
// ============================================================
function buildArchives(parent) {
createFolder(parent, "Clinical_Records_Archived");
createFolder(parent, "Finance_Archived");
var doc = DocumentApp.create("📋 Archives — Quick Reference");
var b = doc.getBody();
h1(b, "Archives — Quick Reference");
bold(b, "When to archive:");
b.appendParagraph("When therapy ends — planned ending, withdrawal or non-attendance.");
b.appendParagraph("");
bold(b, "How to archive:");
b.appendParagraph("Option A (recommended): Practice Tools → Close Client Case in Client_Index");
b.appendParagraph("Option B (manual): Update status to Closed → add end date → add retention deadline → move folder here");
b.appendParagraph("");
bold(b, "When to delete:");
b.appendParagraph("Only after retention deadline has passed.");
b.appendParagraph("UK/Ireland: 7 years after final session.");
b.appendParagraph("Minors: Until age 25 or 7 years, whichever is later.");
b.appendParagraph("");
bold(b, "How to delete:");
b.appendParagraph("1. Permanently delete client folder");
b.appendParagraph("2. Empty Google Drive Trash — this is permanent");
b.appendParagraph("3. Log deletion in Client_Index → Deletion_Log");
b.appendParagraph("4. Update status to Deleted");
b.appendParagraph("");
var pb = b.appendParagraph("⚠️ Emptying Trash is irreversible. Confirm retention period has passed first."); pb.setBold(true);
doc.saveAndClose();
moveFile(doc.getId(), parent);
log("Created: Archives folder");
}
// ============================================================
// UTILITY FUNCTIONS
// ============================================================
function createFolder(parent, name) {
var f = parent.createFolder(name);
log("Folder: " + name);
return f;
}
function moveFile(fileId, folder) {
var file = DriveApp.getFileById(fileId);
folder.addFile(file);
DriveApp.getRootFolder().removeFile(file);
}
function styleHeaderRow(sheet, headers, bgColor) {
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.getRange(1, 1, 1, headers.length)
.setBackground(bgColor).setFontColor("#ffffff")
.setFontWeight("bold").setFontSize(11);
sheet.setFrozenRows(1);
}
function setColumnWidths(sheet, widths) {
for (var i = 0; i < widths.length; i++) {
sheet.setColumnWidth(i + 1, widths[i]);
}
}
function applyDropdown(sheet, startRow, col, numRows, values) {
var rule = SpreadsheetApp.newDataValidation()
.requireValueInList(values, true).build();
sheet.getRange(startRow, col, numRows, 1).setDataValidation(rule);
}
function applyConditionalFormat(sheet, triggerText, bgColor, fontColor, startRow, startCol, numRows, numCols) {
var rule = SpreadsheetApp.newConditionalFormatRule()
.whenTextEqualTo(triggerText)
.setBackground(bgColor).setFontColor(fontColor)
.setRanges([sheet.getRange(startRow, startCol, numRows, numCols)]).build();
sheet.setConditionalFormatRules([rule]);
}
function addNote(sheet, cell, note) {
sheet.getRange(cell).setNote(note);
}
function h1(body, text) {
body.appendParagraph(text).setHeading(DocumentApp.ParagraphHeading.HEADING1);
}
function h2(body, text) {
body.appendParagraph(text).setHeading(DocumentApp.ParagraphHeading.HEADING2);
}
function bold(body, text) {
body.appendParagraph(text).setBold(true);
}
function setRed(par) {
var attrs = {};
attrs[DocumentApp.Attribute.FOREGROUND_COLOR] = "#cc0000";
par.setAttributes(attrs);
}
function log(msg) {
Logger.log(msg);
}"
language="javascriptPractice Tools Menu
Open your new Client_Index spreadsheet, click Extensions → Apps Script, paste this script, save, then click Run → installPracticeTools. Reload the sheet and you'll see the 🏥 Practice Tools menu.
/**
* ============================================================
* PRACTICE INFRASTRUCTURE — STEP 2: PRACTICE TOOLS MENU
* Version 3.3
* ============================================================
* This script lives INSIDE the Client_Index spreadsheet.
*
* HOW TO INSTALL:
* 1. Open your Client_Index spreadsheet
* 2. Click Extensions → Apps Script
* 3. Delete any existing code in the editor
* 4. Paste this entire script
* 5. Click Save (floppy disk icon)
* 6. Click Run → installPracticeTools
* 7. Approve permissions when prompted
* 8. Close the Apps Script tab
* 9. Reload your spreadsheet
* 10. You will see "🏥 Practice Tools" in the menu bar
*
* From then on, the menu loads automatically every time
* you open the spreadsheet.
* ============================================================
*/
// ============================================================
// MENU INSTALLATION — Runs on every open + first install
// ============================================================
function onOpen() {
SpreadsheetApp.getUi()
.createMenu("🏥 Practice Tools")
.addItem("➕ Add New Client", "addNewClient")
.addSeparator()
.addItem("📅 Log Session", "logSession")
.addItem("📋 Log DNA / Late Cancellation", "logDNA")
.addSeparator()
.addItem("📁 Close Client Case", "closeClientCase")
.addSeparator()
.addItem("📄 Generate Superbill (US)", "generateSuperbill")
.addItem("🧾 Generate Invoice / Receipt", "generateInvoice")
.addSeparator()
.addItem("ℹ️ Help", "showHelp")
.addToUi();
}
function installPracticeTools() {
onOpen();
showAlert("✅ Practice Tools Installed",
"The Practice Tools menu has been added to your spreadsheet.\n\n" +
"Close this tab and reload your spreadsheet to see the menu in the top bar.\n\n" +
"Start with: 🏥 Practice Tools → Add New Client");
}
// ============================================================
// HELPER — Get settings from Settings tab
// ============================================================
function getSettings() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName("⚙️ Settings");
if (!s) throw new Error("Settings tab not found. Make sure you ran STEP1 correctly.");
var data = s.getRange("A4:B12").getValues();
var settings = {};
data.forEach(function(row) { if (row[0]) settings[row[0]] = row[1]; });
return settings;
}
function getNextClientCode() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName("👥 Clients");
var data = s.getDataRange().getValues();
var maxCode = 0;
for (var i = 1; i < data.length; i++) {
var code = String(data[i][0]);
if (code.match(/^C\d+$/)) {
var num = parseInt(code.replace("C", ""));
if (num > maxCode) maxCode = num;
}
}
var next = maxCode + 1;
return "C" + String(next).padStart(3, "0");
}
function getClientList() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName("👥 Clients");
var data = s.getDataRange().getValues();
var clients = [];
for (var i = 1; i < data.length; i++) {
if (data[i][0] && data[i][0] !== "Client Code" && data[i][8] === "Active") {
clients.push(data[i][0] + " — " + data[i][1] + " " + data[i][2]);
}
}
return clients;
}
function getClientFee(clientCode) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName("👥 Clients");
var data = s.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === clientCode) {
return data[i][9] || 0; // Default Fee column
}
}
return 0;
}
function getNextSessionNumber(clientCode) {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var s = ss.getSheetByName("📅 Session_Log");
var data = s.getDataRange().getValues();
var max = 0;
for (var i = 1; i < data.length; i++) {
if (data[i][1] === clientCode) {
var n = parseInt(data[i][2]);
if (n > max) max = n;
}
}
return max + 1;
}
// ============================================================
// ADD NEW CLIENT
// ============================================================
function addNewClient() {
var ui = SpreadsheetApp.getUi();
var settings = getSettings();
var nextCode = getNextClientCode();
var firstName = askUser(ui, "ADD NEW CLIENT (1/7)", "Client first name:");
if (!firstName) return;
var lastName = askUser(ui, "ADD NEW CLIENT (2/7)", "Client last name:");
if (!lastName) return;
var email = askUser(ui, "ADD NEW CLIENT (3/7)", "Client email address:");
if (!email) return;
// ── Duplicate email check ──
var ss = SpreadsheetApp.getActiveSpreadsheet();
var clientSheet = ss.getSheetByName("👥 Clients");
var existingData = clientSheet.getDataRange().getValues();
for (var e = 1; e < existingData.length; e++) {
if (existingData[e][3] && existingData[e][3].toString().toLowerCase() === email.toLowerCase()) {
var existingCode = existingData[e][0];
var existingName = existingData[e][1] + " " + existingData[e][2];
var dupResponse = ui.alert(
"⚠️ Duplicate Email Detected",
"This email address is already on record for:\n" +
existingCode + " — " + existingName + "\n\n" +
"Do you want to continue adding this as a new client anyway?",
ui.ButtonSet.YES_NO
);
if (dupResponse !== ui.Button.YES) return;
break;
}
}
var phone = askUser(ui, "ADD NEW CLIENT (4/7)", "Client phone number (or press OK to skip):");
if (!phone) phone = "";
var dobRaw = askUser(ui, "ADD NEW CLIENT (5/7)",
"Client date of birth (used to calculate retention deadline for minor clients).\n\n" +
"Accepted formats:\n" +
" 05/03/1990 | 1990-03-05 | 5 March 1990\n\n" +
"Press OK to skip if not known.");
var dob = dobRaw ? (parseFlexibleDate(dobRaw) || "") : "";
if (dobRaw && !dob) {
showAlert("Invalid Date", "\"" + dobRaw + "\" was not recognised. Date of birth left blank — you can add it manually in the Clients tab.");
dob = "";
}
var startDate = askDate(ui, "ADD NEW CLIENT (6/7)", "Start date:");
if (!startDate) startDate = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd");
var feeInput = askUser(ui, "ADD NEW CLIENT (7/7)",
"Session fee (" + settings["Currency Symbol"] + ") — default is " + settings["Default Session Fee"] + ":\n(Press OK to use default)");
var fee;
if (!feeInput) {
fee = parseFloat(settings["Default Session Fee"]) || 0;
} else {
fee = parseFloat(feeInput);
if (isNaN(fee)) {
showAlert("Invalid Fee", "Please enter a number for the fee.");
return;
}
}
// ── Build client folder ──
var clientFolderName = nextCode + "-" + getInitials(firstName, lastName);
var clinicalFolder = getClinicalRecordsFolder();
if (!clinicalFolder) {
showAlert("Folder Error",
"Could not find the 1_Clinical_Records folder.\n\n" +
"Make sure this spreadsheet is inside the Practice Infrastructure folder structure created by the setup script.");
return;
}
var clientFolder = clinicalFolder.createFolder(clientFolderName);
clientFolder.createFolder("01_Admin");
clientFolder.createFolder("02_Consent_Forms");
clientFolder.createFolder("03_Intake_Forms");
clientFolder.createFolder("04_Session_Notes");
clientFolder.createFolder("05_Client_Materials");
clientFolder.createFolder("06_Correspondence");
var folderUrl = "https://drive.google.com/drive/folders/" + clientFolder.getId();
// ── Add to Clients tab ──
var newRow = [
nextCode, firstName, lastName, email, phone,
dob, startDate, "", "Active",
fee, "",
folderUrl,
"", ""
];
clientSheet.appendRow(newRow);
var lastRow = clientSheet.getLastRow();
clientSheet.getRange(lastRow, 1, 1, newRow.length)
.setFontColor("#000000").setFontStyle("normal");
showAlert("✅ Client Added — " + nextCode,
"Client " + firstName + " " + lastName + " has been added as " + nextCode + ".\n\n" +
"Their folder has been created automatically:\n" +
clientFolderName + "\n\n" +
"NEXT STEPS:\n" +
"1. Send them the intake form\n" +
"2. Send them the consent form and privacy notice\n" +
" (4_Templates_Policies → Consent_Templates)\n" +
"3. Save completed forms to their Drive folder when returned");
}
// Navigate from this spreadsheet up to find 1_Clinical_Records
function getPracticeFolder() {
try {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var ssFile = DriveApp.getFileById(ss.getId());
var parents = ssFile.getParents();
while (parents.hasNext()) {
var adminFolder = parents.next();
var grandParents = adminFolder.getParents();
while (grandParents.hasNext()) {
var practiceFolder = grandParents.next();
var subs = practiceFolder.getFolders();
while (subs.hasNext()) {
var sub = subs.next();
if (sub.getName().indexOf("1_Clinical_Records") !== -1) {
return practiceFolder;
}
}
}
}
return null;
} catch(e) {
return null;
}
}
function getClinicalRecordsFolder() {
try {
var practiceFolder = getPracticeFolder();
if (!practiceFolder) return null;
var subs = practiceFolder.getFolders();
while (subs.hasNext()) {
var sub = subs.next();
if (sub.getName().indexOf("1_Clinical_Records") !== -1) return sub;
}
return null;
} catch(e) {
return null;
}
}
// ============================================================
// LOG SESSION
// ============================================================
function logSession() {
var ui = SpreadsheetApp.getUi();
var settings = getSettings();
var clients = getClientList();
if (clients.length === 0) {
showAlert("No Active Clients", "Add a client first using Practice Tools → Add New Client.");
return;
}
var clientMenu = clients.map(function(c, i) { return (i+1) + ". " + c; }).join("\n");
var clientIdx = askNumber(ui, "LOG SESSION (1/6)",
"Type the NUMBER of the client (e.g. type 1, not the client name):\n\n" + clientMenu);
if (clientIdx === null) return;
if (clientIdx < 1 || clientIdx > clients.length) {
showAlert("Invalid Selection", "Please enter a number between 1 and " + clients.length + ".");
return;
}
var clientCode = clients[clientIdx - 1].split(" — ")[0];
var clientFee = getClientFee(clientCode);
var nextSession = getNextSessionNumber(clientCode);
var sessionDate = askDate(ui, "LOG SESSION (2/6)", "Session date:");
if (!sessionDate) return;
var formatIdx = askNumber(ui, "LOG SESSION (3/6)",
"Type the NUMBER for session format:\n\n" +
"1. Video — Google Meet\n2. In Person\n3. Phone\n4. Group");
if (formatIdx === null) return;
var formats = ["Video — Google Meet", "In Person", "Phone", "Group"];
if (formatIdx < 1 || formatIdx > 4) {
showAlert("Invalid Selection", "Please enter a number between 1 and 4.");
return;
}
var format = formats[formatIdx - 1];
var defaultDuration = settings["Standard Session Length (mins)"] || 50;
var durationInput = askUser(ui, "LOG SESSION (4/6)",
"Session duration in minutes.\nPress OK to use standard " + defaultDuration + " mins:");
var duration = durationInput ? parseInt(durationInput) : defaultDuration;
if (isNaN(duration) || duration <= 0) duration = defaultDuration;
var feeInput = askUser(ui, "LOG SESSION (5/6)",
"Fee charged (" + settings["Currency Symbol"] + ").\n" +
"Standard fee for this client is " + settings["Currency Symbol"] + clientFee + ".\n" +
"Press OK to use standard fee:");
var fee;
if (!feeInput) {
fee = clientFee;
} else {
fee = parseFloat(feeInput);
if (isNaN(fee)) {
showAlert("Invalid Fee", "Please enter a number for the fee.");
return;
}
}
var paidIdx = askNumber(ui, "LOG SESSION (6/6)",
"Type the NUMBER for payment status:\n\n1. Yes\n2. No\n3. Waived");
if (paidIdx === null) return;
var paidOptions = ["Yes", "No", "Waived"];
if (paidIdx < 1 || paidIdx > 3) {
showAlert("Invalid Selection", "Please enter 1, 2, or 3.");
return;
}
var paid = paidOptions[paidIdx - 1];
var paymentDate = (paid === "Yes") ? sessionDate : "";
var invoiceRef = "INV-" + clientCode + "-" + String(nextSession).padStart(3, "0");
var ss = SpreadsheetApp.getActiveSpreadsheet();
var logSheet = ss.getSheetByName("📅 Session_Log");
logSheet.appendRow([
sessionDate, clientCode, nextSession,
duration, format, fee, paid, paymentDate, invoiceRef, "", ""
]);
var lastRow = logSheet.getLastRow();
logSheet.getRange(lastRow, 1, 1, 11).setFontColor("#000000").setFontStyle("normal");
if (paid === "No") {
logSheet.getRange(lastRow, 7).setBackground("#fff3cd");
}
showAlert("✅ Session Logged",
"Session " + nextSession + " for " + clientCode + " logged on " + formatDisplayDate(sessionDate) + ".\n" +
"Fee: " + settings["Currency Symbol"] + fee + " | Paid: " + paid + "\n\n" +
"Remember to write your session note and save it to:\n" +
"1_Clinical_Records → " + clientCode + " → 04_Session_Notes\n" +
"Filename: " + sessionDate + "_" + clientCode + "_SessionNote");
}
// ============================================================
// LOG DNA / LATE CANCELLATION
// ============================================================
function logDNA() {
var ui = SpreadsheetApp.getUi();
var clients = getClientList();
if (clients.length === 0) {
showAlert("No Active Clients", "No active clients found.");
return;
}
var clientMenu = clients.map(function(c, i) { return (i+1) + ". " + c; }).join("\n");
var clientIdx = askNumber(ui, "LOG DNA (1/4)",
"Type the NUMBER of the client (e.g. type 1, not the client name):\n\n" + clientMenu);
if (clientIdx === null) return;
if (clientIdx < 1 || clientIdx > clients.length) {
showAlert("Invalid Selection", "Please enter a number between 1 and " + clients.length + ".");
return;
}
var clientCode = clients[clientIdx - 1].split(" — ")[0];
var dnaDate = askDate(ui, "LOG DNA (2/4)", "Date of missed session:");
if (!dnaDate) return;
var sessionNo = getNextSessionNumber(clientCode);
var contactIdx = askNumber(ui, "LOG DNA (3/4)",
"Type the NUMBER for contact attempted:\n\n" +
"1. Yes — Email\n2. Yes — Phone\n3. Yes — Both\n4. No contact attempted");
if (contactIdx === null) return;
var contactOptions = ["Yes — Email", "Yes — Phone", "Yes — Both", "No contact attempted"];
if (contactIdx < 1 || contactIdx > 4) {
showAlert("Invalid Selection", "Please enter a number between 1 and 4.");
return;
}
var contact = contactOptions[contactIdx - 1];
var safeguardingIdx = askNumber(ui, "LOG DNA (4/4)",
"Type the NUMBER for safeguarding concern:\n\n1. No\n2. Yes — noted\n3. Yes — escalated");
if (safeguardingIdx === null) return;
var safeguardingOptions = ["No", "Yes — noted", "Yes — escalated"];
if (safeguardingIdx < 1 || safeguardingIdx > 3) {
showAlert("Invalid Selection", "Please enter 1, 2, or 3.");
return;
}
var safeguarding = safeguardingOptions[safeguardingIdx - 1];
var ss = SpreadsheetApp.getActiveSpreadsheet();
var dnaSheet = ss.getSheetByName("📋 DNA_Log");
dnaSheet.appendRow([dnaDate, clientCode, sessionNo, contact, "", "", safeguarding, ""]);
var lastRow = dnaSheet.getLastRow();
dnaSheet.getRange(lastRow, 1, 1, 8).setFontColor("#000000").setFontStyle("normal");
if (safeguarding !== "No") {
dnaSheet.getRange(lastRow, 7).setBackground("#ffcccc");
showAlert("⚠️ Safeguarding Concern Flagged",
"DNA logged for " + clientCode + " on " + formatDisplayDate(dnaDate) + ".\n\n" +
"A safeguarding concern has been noted. Consider:\n" +
"• Bringing to supervision\n" +
"• Contacting the client's emergency contact if appropriate\n" +
"• Documenting all actions taken in the DNA_Log notes column");
} else {
showAlert("✅ DNA Logged",
"Did Not Attend logged for " + clientCode + " on " + formatDisplayDate(dnaDate) + ".");
}
}
// ============================================================
// CLOSE CLIENT CASE
// ============================================================
function closeClientCase() {
var ui = SpreadsheetApp.getUi();
var ss = SpreadsheetApp.getActiveSpreadsheet(); // ← declared at top to avoid reference bug
var clients = getClientList();
if (clients.length === 0) {
showAlert("No Active Clients", "No active clients to close.");
return;
}
var clientMenu = clients.map(function(c, i) { return (i+1) + ". " + c; }).join("\n");
var clientIdx = askNumber(ui, "CLOSE CLIENT CASE (1/3)",
"Type the NUMBER of the client to close (e.g. type 1, not the client name):\n\n" + clientMenu);
if (clientIdx === null) return;
if (clientIdx < 1 || clientIdx > clients.length) {
showAlert("Invalid Selection", "Please enter a number between 1 and " + clients.length + ".");
return;
}
var clientCode = clients[clientIdx - 1].split(" — ")[0];
var endDate = askDate(ui, "CLOSE CLIENT CASE (2/3)", "Date of final session:");
if (!endDate) return;
var minorIdx = askNumber(ui, "CLOSE CLIENT CASE (3/3)",
"Type the NUMBER for minor status:\n\n" +
"1. No — adult client\n2. Yes — was a minor at any point during therapy");
if (minorIdx === null) return;
if (minorIdx < 1 || minorIdx > 2) {
showAlert("Invalid Selection", "Please enter 1 or 2.");
return;
}
var wasMinor = (minorIdx === 2);
// Calculate retention deadline
var endDateObj = new Date(endDate);
var sevenYearsAfterEnd = new Date(endDateObj);
sevenYearsAfterEnd.setFullYear(sevenYearsAfterEnd.getFullYear() + 7);
var retentionDate = sevenYearsAfterEnd;
var retentionNote = "7 years after final session";
if (wasMinor) {
var clientSheet2 = ss.getSheetByName("👥 Clients");
var clientData2 = clientSheet2.getDataRange().getValues();
var clientDOB = null;
for (var k = 1; k < clientData2.length; k++) {
if (clientData2[k][0] === clientCode && clientData2[k][5]) {
clientDOB = new Date(clientData2[k][5]);
break;
}
}
if (clientDOB && !isNaN(clientDOB)) {
var age25Date = new Date(clientDOB);
age25Date.setFullYear(age25Date.getFullYear() + 25);
if (age25Date > sevenYearsAfterEnd) {
retentionDate = age25Date;
retentionNote = "Until age 25 (longer than 7-year rule)";
} else {
retentionNote = "7 years after final session (longer than until age 25)";
}
} else {
var fallbackDate = new Date(endDateObj);
fallbackDate.setFullYear(fallbackDate.getFullYear() + 25);
retentionDate = fallbackDate;
retentionNote = "Minor — no DOB recorded. Conservative 25-year deadline set. Update once DOB confirmed.";
}
}
var retentionStr = Utilities.formatDate(retentionDate, Session.getScriptTimeZone(), "yyyy-MM-dd");
// Update Clients tab
var clientSheet = ss.getSheetByName("👥 Clients");
var data = clientSheet.getDataRange().getValues();
for (var i = 1; i < data.length; i++) {
if (data[i][0] === clientCode) {
clientSheet.getRange(i + 1, 8).setValue(endDate);
clientSheet.getRange(i + 1, 9).setValue("Closed");
clientSheet.getRange(i + 1, 11).setValue(retentionStr);
break;
}
}
// ── Auto-move client folder to 5_Archives ──
var moved = false;
try {
var practiceFolder = getPracticeFolder();
var clinicalFolder = null;
var archivesFolder = null;
if (practiceFolder) {
var subs = practiceFolder.getFolders();
while (subs.hasNext()) {
var sub = subs.next();
var subName = sub.getName();
if (subName.indexOf("1_Clinical_Records") !== -1) clinicalFolder = sub;
if (subName.indexOf("5_Archives") !== -1) archivesFolder = sub;
}
}
var archivedSubfolder = null;
if (archivesFolder) {
var archiveSubs = archivesFolder.getFolders();
while (archiveSubs.hasNext()) {
var archiveSub = archiveSubs.next();
if (archiveSub.getName().indexOf("Clinical_Records_Archived") !== -1) {
archivedSubfolder = archiveSub;
}
}
}
if (clinicalFolder && archivedSubfolder) {
var clientFolders = clinicalFolder.getFolders();
while (clientFolders.hasNext()) {
var clientFolder = clientFolders.next();
if (clientFolder.getName().indexOf(clientCode) !== -1) {
archivedSubfolder.addFolder(clientFolder);
clinicalFolder.removeFolder(clientFolder);
moved = true;
break;
}
}
}
} catch(e) {}
var moveMsg = moved
? "✅ Their folder has been moved to 5_Archives automatically."
: "⚠️ Could not move folder automatically.\nPlease move it manually:\nFROM: 1_Clinical_Records → " + clientCode + "\nTO: 5_Archives → Clinical_Records_Archived";
showAlert("✅ Client Case Closed — " + clientCode,
"Status: Closed\n" +
"End date: " + formatDisplayDate(endDate) + "\n" +
"Retention deadline: " + formatDisplayDate(retentionStr) + "\n" +
"Basis: " + retentionNote + "\n\n" +
moveMsg + "\n\n" +
"Reminder: Complete the Therapy Ending Summary template\n" +
"(4_Templates_Policies → Clinical_Templates)");
}
// ============================================================
// GENERATE SUPERBILL (US)
// ============================================================
function generateSuperbill() {
var ui = SpreadsheetApp.getUi();
var settings = getSettings();
var clients = getClientList();
if (clients.length === 0) {
showAlert("No Active Clients", "No active clients found.");
return;
}
var clientMenu = clients.map(function(c, i) { return (i+1) + ". " + c; }).join("\n");
var clientIdx = askNumber(ui, "GENERATE SUPERBILL (1/3)",
"Type the NUMBER of the client (e.g. type 1, not the client name):\n\n" + clientMenu);
if (clientIdx === null) return;
if (clientIdx < 1 || clientIdx > clients.length) {
showAlert("Invalid Selection", "Please enter a number between 1 and " + clients.length + ".");
return;
}
var clientCode = clients[clientIdx - 1].split(" — ")[0];
var clientName = clients[clientIdx - 1].split(" — ")[1];
var periodInput = askUser(ui, "GENERATE SUPERBILL (2/3)",
"Billing period (e.g. January 2026 or 2026-01):");
if (!periodInput) return;
var diagnosisInput = askUser(ui, "GENERATE SUPERBILL (3/3)",
"Primary ICD-10 diagnosis code (optional — leave blank if not applicable):\n\n" +
"⚠️ Only enter if you are licensed to diagnose in your jurisdiction.");
if (!diagnosisInput) diagnosisInput = "[Not provided — see note]";
// Pull sessions for this client from Session_Log
var ss = SpreadsheetApp.getActiveSpreadsheet();
var logSheet = ss.getSheetByName("📅 Session_Log");
var logData = logSheet.getDataRange().getValues();
var sessions = [];
var totalCharged = 0;
var totalPaid = 0;
// Parse billing period for filtering — accept "January 2026" or "2026-01"
var filterMonth = -1;
var filterYear = -1;
var monthNames = ["january","february","march","april","may","june","july","august","september","october","november","december"];
var periodLower = periodInput.toLowerCase().trim();
var periodParts = periodLower.split(/[\s\-\/]+/);
for (var p = 0; p < periodParts.length; p++) {
var mIdx = monthNames.indexOf(periodParts[p]);
if (mIdx !== -1) filterMonth = mIdx + 1;
var maybeYear = parseInt(periodParts[p]);
if (!isNaN(maybeYear) && maybeYear > 2000) filterYear = maybeYear;
}
// "2026-01" format
if (filterMonth === -1 && periodParts.length >= 2) {
var y = parseInt(periodParts[0]), m = parseInt(periodParts[1]);
if (!isNaN(y) && !isNaN(m)) { filterYear = y; filterMonth = m; }
}
for (var i = 1; i < logData.length; i++) {
if (logData[i][1] === clientCode && logData[i][0] !== "Date" && logData[i][0] !== "") {
var rawDate = logData[i][0];
var rowDate = (rawDate instanceof Date) ? rawDate : new Date(rawDate);
if (isNaN(rowDate.getTime())) continue;
// Apply period filter if we successfully parsed it
if (filterMonth !== -1 && filterYear !== -1) {
if (rowDate.getMonth() + 1 !== filterMonth || rowDate.getFullYear() !== filterYear) continue;
}
var sessionDate = Utilities.formatDate(rowDate, Session.getScriptTimeZone(), "yyyy-MM-dd");
var fee = parseFloat(logData[i][5]) || 0;
var paid = logData[i][6];
var cptCode = logData[i][9] || "90837";
sessions.push({
date: sessionDate,
duration: logData[i][3],
fee: fee,
paid: paid === "Yes" ? fee : 0,
cpt: cptCode
});
totalCharged += fee;
if (paid === "Yes") totalPaid += fee;
}
}
if (sessions.length === 0) {
showAlert("No Sessions Found", "No logged sessions found for " + clientCode + ".");
return;
}
// Get client details
var clientSheet = ss.getSheetByName("👥 Clients");
var clientData = clientSheet.getDataRange().getValues();
var clientDOB = "";
for (var j = 1; j < clientData.length; j++) {
if (clientData[j][0] === clientCode) {
clientDOB = clientData[j][5] || "Not recorded";
break;
}
}
// Create superbill document
var doc = DocumentApp.create("Superbill — " + clientCode + " — " + periodInput);
var b = doc.getBody();
b.appendParagraph("SUPERBILL").setHeading(DocumentApp.ParagraphHeading.HEADING1);
b.appendParagraph("Statement Date: " + formatDisplayDate(Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd")));
b.appendParagraph("Billing Period: " + periodInput);
b.appendParagraph("");
b.appendParagraph("Provider Information").setHeading(DocumentApp.ParagraphHeading.HEADING2);
b.appendParagraph("Provider: " + settings["Therapist Name"]);
b.appendParagraph("Practice: " + settings["Practice Name"]);
b.appendParagraph("Credentials: [ADD CREDENTIALS]");
b.appendParagraph("NPI: [ADD NPI NUMBER]");
b.appendParagraph("Tax ID / EIN: [ADD TAX ID]");
b.appendParagraph("Email: " + settings["Practice Email"]);
b.appendParagraph("");
b.appendParagraph("Client Information").setHeading(DocumentApp.ParagraphHeading.HEADING2);
b.appendParagraph("Client: " + clientName);
b.appendParagraph("Client Code: " + clientCode);
b.appendParagraph("Date of Birth: " + clientDOB);
b.appendParagraph("Insurance Provider: [CLIENT TO ADD]");
b.appendParagraph("Member ID: [CLIENT TO ADD]");
b.appendParagraph("");
b.appendParagraph("Diagnosis").setHeading(DocumentApp.ParagraphHeading.HEADING2);
b.appendParagraph("Primary ICD-10: " + diagnosisInput);
b.appendParagraph("⚠️ Diagnosis codes must be assigned by a licensed clinician only.").setItalic(true);
b.appendParagraph("");
b.appendParagraph("Services Rendered").setHeading(DocumentApp.ParagraphHeading.HEADING2);
b.appendParagraph("Date | CPT | Description | Mins | Fee | Paid");
b.appendParagraph("─────────────────────────────────────────────────────────────");
sessions.forEach(function(s) {
b.appendParagraph(
s.date + " | " + s.cpt + " | Individual Psychotherapy | " +
s.duration + " | " + settings["Currency Symbol"] + s.fee +
" | " + settings["Currency Symbol"] + s.paid
);
});
b.appendParagraph("─────────────────────────────────────────────────────────────");
b.appendParagraph("TOTAL CHARGED: " + settings["Currency Symbol"] + totalCharged.toFixed(2) +
" | TOTAL PAID: " + settings["Currency Symbol"] + totalPaid.toFixed(2) +
" | BALANCE: " + settings["Currency Symbol"] + (totalCharged - totalPaid).toFixed(2));
b.appendParagraph("");
b.appendParagraph("Provider Certification").setHeading(DocumentApp.ParagraphHeading.HEADING2);
b.appendParagraph("I certify that the services listed above were medically necessary and personally rendered by me.");
b.appendParagraph("");
b.appendParagraph("Signature: ___________________________________________ Date: ___________");
doc.saveAndClose();
// Auto-move to 3_Finance → Superbills
var superbillMoved = false;
try {
var practiceFolder = getPracticeFolder();
if (practiceFolder) {
var pSubs = practiceFolder.getFolders();
while (pSubs.hasNext()) {
var pSub = pSubs.next();
if (pSub.getName().indexOf("3_Finance") !== -1) {
var fSubs = pSub.getFolders();
while (fSubs.hasNext()) {
var fSub = fSubs.next();
if (fSub.getName() === "Superbills") {
var docFile = DriveApp.getFileById(doc.getId());
fSub.addFile(docFile);
DriveApp.getRootFolder().removeFile(docFile);
superbillMoved = true;
break;
}
}
break;
}
}
}
} catch(e) {}
var sbLocationMsg = superbillMoved
? "Saved to: 3_Finance → Superbills"
: "Created in your Drive root — move to: 3_Finance → Superbills";
showAlert("✅ Superbill Created",
"Superbill generated for " + clientCode + " — " + periodInput + ".\n\n" +
"Total charged: " + settings["Currency Symbol"] + totalCharged.toFixed(2) + "\n" +
"Total paid: " + settings["Currency Symbol"] + totalPaid.toFixed(2) + "\n\n" +
sbLocationMsg + "\n\n" +
"Remember to add your NPI number and credentials before sending.");
}
// ============================================================
// GENERATE INVOICE / RECEIPT
// ============================================================
function generateInvoice() {
var ui = SpreadsheetApp.getUi();
var settings = getSettings();
var clients = getClientList();
if (clients.length === 0) {
showAlert("No Active Clients", "No active clients found.");
return;
}
var clientMenu = clients.map(function(c, i) { return (i+1) + ". " + c; }).join("\n");
var clientIdx = askNumber(ui, "GENERATE INVOICE (1/2)",
"Type the NUMBER of the client (e.g. type 1, not the client name):\n\n" + clientMenu);
if (clientIdx === null) return;
if (clientIdx < 1 || clientIdx > clients.length) {
showAlert("Invalid Selection", "Please enter a number between 1 and " + clients.length + ".");
return;
}
var clientCode = clients[clientIdx - 1].split(" — ")[0];
var clientName = clients[clientIdx - 1].split(" — ")[1];
var periodInput = askUser(ui, "GENERATE INVOICE (2/2)", "Billing period (e.g. January 2026):");
if (!periodInput) return;
// Pull sessions from log
var ss = SpreadsheetApp.getActiveSpreadsheet();
var logSheet = ss.getSheetByName("📅 Session_Log");
var logData = logSheet.getDataRange().getValues();
var sessions = [];
var totalFee = 0;
var totalPaid = 0;
// Parse billing period for filtering
var filterMonth = -1;
var filterYear = -1;
var monthNames = ["january","february","march","april","may","june","july","august","september","october","november","december"];
var periodLower = periodInput.toLowerCase().trim();
var periodParts = periodLower.split(/[\s\-\/]+/);
for (var p = 0; p < periodParts.length; p++) {
var mIdx = monthNames.indexOf(periodParts[p]);
if (mIdx !== -1) filterMonth = mIdx + 1;
var maybeYear = parseInt(periodParts[p]);
if (!isNaN(maybeYear) && maybeYear > 2000) filterYear = maybeYear;
}
if (filterMonth === -1 && periodParts.length >= 2) {
var y = parseInt(periodParts[0]), m = parseInt(periodParts[1]);
if (!isNaN(y) && !isNaN(m)) { filterYear = y; filterMonth = m; }
}
for (var i = 1; i < logData.length; i++) {
if (logData[i][1] === clientCode && logData[i][0] !== "Date" && logData[i][0] !== "") {
var rawDate = logData[i][0];
var rowDate = (rawDate instanceof Date) ? rawDate : new Date(rawDate);
if (isNaN(rowDate.getTime())) continue;
if (filterMonth !== -1 && filterYear !== -1) {
if (rowDate.getMonth() + 1 !== filterMonth || rowDate.getFullYear() !== filterYear) continue;
}
var fee = parseFloat(logData[i][5]) || 0;
var paid = logData[i][6];
var displayDate = Utilities.formatDate(rowDate, Session.getScriptTimeZone(), "yyyy-MM-dd");
sessions.push({
date: displayDate,
duration: logData[i][3],
fee: fee,
invoiceRef: logData[i][8],
paid: paid
});
totalFee += fee;
if (paid === "Yes") totalPaid += fee;
}
}
if (sessions.length === 0) {
showAlert("No Sessions Found", "No logged sessions for " + clientCode + ".");
return;
}
var invoiceNumber = "INV-" + clientCode + "-" + new Date().getFullYear() +
"-" + String(new Date().getMonth() + 1).padStart(2, "0");
var doc = DocumentApp.create("Invoice — " + clientCode + " — " + periodInput);
var b = doc.getBody();
b.appendParagraph("INVOICE").setHeading(DocumentApp.ParagraphHeading.HEADING1);
b.appendParagraph("Invoice No: " + invoiceNumber);
b.appendParagraph("Date: " + formatDisplayDate(Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd")));
b.appendParagraph("Period: " + periodInput);
b.appendParagraph("");
b.appendParagraph("From").setHeading(DocumentApp.ParagraphHeading.HEADING2);
b.appendParagraph(settings["Therapist Name"]);
b.appendParagraph(settings["Practice Name"]);
b.appendParagraph(settings["Practice Email"]);
b.appendParagraph("");
b.appendParagraph("To").setHeading(DocumentApp.ParagraphHeading.HEADING2);
b.appendParagraph(clientName + " (Client Ref: " + clientCode + ")");
b.appendParagraph("");
b.appendParagraph("Sessions").setHeading(DocumentApp.ParagraphHeading.HEADING2);
b.appendParagraph("Date | Duration | Description | Fee | Status");
b.appendParagraph("─────────────────────────────────────────────────────");
sessions.forEach(function(s) {
b.appendParagraph(
s.date + " | " + s.duration + " min | Therapy Session | " +
settings["Currency Symbol"] + s.fee + " | " + s.paid
);
});
b.appendParagraph("─────────────────────────────────────────────────────");
b.appendParagraph("TOTAL: " + settings["Currency Symbol"] + totalFee.toFixed(2) +
" | PAID: " + settings["Currency Symbol"] + totalPaid.toFixed(2) +
" | OUTSTANDING: " + settings["Currency Symbol"] + (totalFee - totalPaid).toFixed(2));
b.appendParagraph("");
doc.saveAndClose();
// Auto-move to 3_Finance → Invoices
var invoiceMoved = false;
try {
var practiceFolder = getPracticeFolder();
if (practiceFolder) {
var pSubs = practiceFolder.getFolders();
while (pSubs.hasNext()) {
var pSub = pSubs.next();
if (pSub.getName().indexOf("3_Finance") !== -1) {
var fSubs = pSub.getFolders();
while (fSubs.hasNext()) {
var fSub = fSubs.next();
if (fSub.getName() === "Invoices") {
var docFile = DriveApp.getFileById(doc.getId());
fSub.addFile(docFile);
DriveApp.getRootFolder().removeFile(docFile);
invoiceMoved = true;
break;
}
}
break;
}
}
}
} catch(e) {}
var locationMsg = invoiceMoved
? "Saved to: 3_Finance → Invoices"
: "Created in your Drive root — move to: 3_Finance → Invoices";
showAlert("✅ Invoice Created",
"Invoice generated for " + clientCode + " — " + periodInput + ".\n\n" +
"Total: " + settings["Currency Symbol"] + totalFee.toFixed(2) + "\n" +
"Outstanding: " + settings["Currency Symbol"] + (totalFee - totalPaid).toFixed(2) + "\n\n" +
locationMsg);
}
// ============================================================
// HELP
// ============================================================
function showHelp() {
showAlert("🏥 Practice Tools — Help",
"MENU OPTIONS:\n\n" +
"➕ Add New Client\n" +
"Assigns a client code, creates their Drive folder, and adds them to the Client Index.\n\n" +
"📅 Log Session\n" +
"Records a completed session with date, format, fee and payment status.\n\n" +
"📋 Log DNA\n" +
"Records a missed or cancelled session and flags safeguarding concerns.\n\n" +
"📁 Close Client Case\n" +
"Updates client status, records end date, calculates retention deadline, and moves folder to Archives.\n\n" +
"📄 Generate Superbill (US)\n" +
"Creates a superbill from logged sessions for insurance reimbursement. US therapists only.\n\n" +
"🧾 Generate Invoice\n" +
"Creates an invoice from logged sessions. You may want to format it before sending.\n\n" +
"─────────────────────────\n" +
"TIPS:\n" +
"• When selecting from a numbered list, type the NUMBER only (e.g. type 2, not the client name).\n" +
"• Dates accept many formats: today, yesterday, 05/03/2026, 5 March 2026.\n" +
"• The Income Dashboard year can be changed in cell E4 of that tab.\n" +
"• Invoice and superbill generation pull from Session_Log automatically.\n\n" +
"For support, contact your practice infrastructure provider.");
}
// ============================================================
// ON EDIT — Clear yellow highlight when Paid status changes
// ============================================================
function onEdit(e) {
try {
var sheet = e.range.getSheet();
if (sheet.getName() !== "📅 Session_Log") return;
// Column 7 = Paid
if (e.range.getColumn() !== 7) return;
var row = e.range.getRow();
if (row < 2) return; // skip header
var value = e.range.getValue();
var rowRange = sheet.getRange(row, 1, 1, 11);
if (value === "Yes" || value === "Waived") {
rowRange.setBackground("#ffffff");
} else if (value === "No") {
sheet.getRange(row, 7).setBackground("#fff3cd"); // yellow on paid cell only
}
} catch(err) {
// Silent — onEdit must never throw to the user
}
}
// ============================================================
// UTILITY
// ============================================================
function askUser(ui, title, message) {
var response = ui.prompt(title, message, ui.ButtonSet.OK_CANCEL);
if (response.getSelectedButton() !== ui.Button.OK) return null;
var text = response.getResponseText().trim();
return text || null;
}
function showAlert(title, message) {
SpreadsheetApp.getUi().alert(title, message, SpreadsheetApp.getUi().ButtonSet.OK);
}
function getInitials(firstName, lastName) {
return (firstName.charAt(0) + lastName.charAt(0)).toUpperCase();
}
/**
* Parses a flexible date input into a YYYY-MM-DD string.
* Accepts: "today", "yesterday", "2026-03-05", "05/03/2026",
* "5 March 2026", "March 5 2026", "05-03-2026"
* Returns null if unparseable.
*/
function parseFlexibleDate(input) {
if (!input) return null;
var s = input.trim().toLowerCase();
if (s === "today") {
return Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyy-MM-dd");
}
if (s === "yesterday") {
var y = new Date(); y.setDate(y.getDate() - 1);
return Utilities.formatDate(y, Session.getScriptTimeZone(), "yyyy-MM-dd");
}
var monthNames = {
january:1, jan:1, february:2, feb:2, march:3, mar:3,
april:4, apr:4, may:5, june:6, jun:6,
july:7, jul:7, august:8, aug:8, september:9, sep:9, sept:9,
october:10, oct:10, november:11, nov:11, december:12, dec:12
};
// YYYY-MM-DD or YYYY/MM/DD or YYYY-DD-MM (try both orderings)
var isoMatch = s.match(/^(\d{4})[-\/](\d{1,2})[-\/](\d{1,2})$/);
if (isoMatch) {
var yr = parseInt(isoMatch[1]);
var a = parseInt(isoMatch[2]);
var b = parseInt(isoMatch[3]);
// Assume YYYY-MM-DD
var d = new Date(yr, a - 1, b);
if (!isNaN(d.getTime())) return Utilities.formatDate(d, Session.getScriptTimeZone(), "yyyy-MM-dd");
}
// DD/MM/YYYY or DD-MM-YYYY
var dmyMatch = s.match(/^(\d{1,2})[-\/](\d{1,2})[-\/](\d{4})$/);
if (dmyMatch) {
var d2 = new Date(parseInt(dmyMatch[3]), parseInt(dmyMatch[2]) - 1, parseInt(dmyMatch[1]));
if (!isNaN(d2.getTime())) return Utilities.formatDate(d2, Session.getScriptTimeZone(), "yyyy-MM-dd");
}
// "5 March 2026" or "March 5 2026"
var parts = s.split(/[\s,]+/);
var day = null, month = null, year = null;
parts.forEach(function(p) {
var n = parseInt(p);
if (!isNaN(n) && n > 31) { year = n; }
else if (!isNaN(n) && n >= 1 && n <= 31 && day === null) { day = n; }
else if (monthNames[p]) { month = monthNames[p]; }
});
if (day && month && year) {
var d3 = new Date(year, month - 1, day);
if (!isNaN(d3.getTime())) return Utilities.formatDate(d3, Session.getScriptTimeZone(), "yyyy-MM-dd");
}
return null;
}
function formatDisplayDate(isoString) {
if (!isoString) return "";
var d = new Date(isoString);
if (isNaN(d.getTime())) return isoString;
var months = ["January","February","March","April","May","June",
"July","August","September","October","November","December"];
return d.getDate() + " " + months[d.getMonth()] + " " + d.getFullYear();
}
function askDate(ui, stepLabel, promptText) {
var raw = askUser(ui, stepLabel,
promptText + "\n\nAccepted formats:\n" +
" today | yesterday\n" +
" 05/03/2026 | 2026-03-05\n" +
" 5 March 2026");
if (!raw) return null;
var parsed = parseFlexibleDate(raw);
if (!parsed) {
showAlert("Invalid Date", "\"" + raw + "\" was not recognised as a valid date.\n\nTry: today, 05/03/2026, or 5 March 2026.");
return null;
}
return parsed;
}
function askNumber(ui, stepLabel, promptText) {
var raw = askUser(ui, stepLabel, promptText);
if (!raw) return null;
var n = parseInt(raw);
if (isNaN(n)) {
showAlert("Invalid Entry", "Please type a NUMBER only (e.g. type 1, not the full text).");
return null;
}
return n;
}"
language="javascriptWant this done for you?
We set up the whole infrastructure — Drive, booking, payments, website — so you can focus on your clients.
Start a Project →