sync: auto-sync from HOWARD-HOME at 2026-04-22 16:24:58

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-04-22 16:24:58
This commit is contained in:
2026-04-22 16:24:58 -07:00
parent ce52a62ff1
commit 7bffbfbb89
8 changed files with 1275 additions and 46 deletions

View File

@@ -101,12 +101,13 @@ All UPNs above use the `@cascadestucson.com` suffix (standard).
## Conflict / verify before creating
- **Christine Nyanzunda** already exists in AD as **Memory Care Admin Assistant** (`Christine.Nyanzunda`, susan.hicks@ department peer — see `docs/servers/active-directory.md` and existing M365 match in `docs/cloud/m365.md`). The caregiver list entry `Christine Nyanzunda-AM shift/MC MED TECH` is likely the same person picking up clinical shifts, not a second identity. **Do not create a second account.** Confirm with Shelby Trozzi / Meredith that her caregiver shifts should use the existing `christine.nyanzunda@` mailbox.
- **Paty Doran** — spelling could be Patricia / Paty / Patti. Confirm with HR before creating.
- **Polett Pinazavala** — unusual spelling, verify with HR.
- **Patricia Sandoval-Beck** — hyphenated last name; SamAccountName may need to be `Patricia.SandovalBeck` if hyphens are disallowed in downstream tools (ALIS, MDM).
- **Ederick Yuzon** — verify spelling.
- **Maia Baker** — name on Sheet2 only, no shift/phone data. Confirm employment status with HR.
- **Christine Nyanzunda** **Resolved 2026-04-22:** one person, one account. Existing `christine.nyanzunda@` mailbox covers both MC Admin role and her part-time Sun/Mon MedTech shifts. Do not create a second account.
- **Paty Doran** — **Resolved 2026-04-22:** legal name `Patricia Camarena Doran`. Account will be `patricia.doran@`.
- **Polett Pinazavala** — **Resolved 2026-04-22:** spelling confirmed; still employed. Setup declared in 2026-04-22 email: MedTech — Memory Care (TueSat), D+P, ALIS=Y, Outside=N.
- **Patricia Sandoval-Beck** — **Resolved 2026-04-22 (CSV inline note from Meredith):** hyphen is correct. SamAccountName may still need to be `Patricia.SandovalBeck` if ALIS/MDM reject hyphens — test during Wave 3.
- **Ederick Yuzon** — **Still pending:** spelling asked in 2026-04-22 email.
- **Maia Baker** — **Resolved 2026-04-22 (CSV inline note):** part-time, still employed.
- **Reliable Agency shared logins (x2)** — new since 2026-04-22: John added two agency rows to the returned CSV without per-person names. Decision: two shared-login accounts, not per-person. Usernames pending (`reliable1@` / `reliable2@` proposed 2026-04-22).
## Licensing plan (when ready — NOT now)
@@ -167,8 +168,10 @@ Group-policy impact: the `CSC - Folder Redirection (LE)` work done for Life Enri
## Open items / decisions needed from client
- [ ] Confirm Christine Nyanzunda is one person, not two (existing M365 account keeps working for caregiver shifts)
- [ ] HR spelling confirmation on Paty Doran, Polett Pinazavala, Patricia Sandoval-Beck, Ederick Yuzon, Maia Baker
- [x] ~~Confirm Christine Nyanzunda is one person, not two~~ (resolved 2026-04-22 — one person, one account)
- [x] ~~HR spelling confirmation on Paty Doran, Polett Pinazavala, Patricia Sandoval-Beck, Maia Baker~~ (all resolved 2026-04-22)
- [ ] **Ederick Yuzon first-name spelling** — asked in 2026-04-22 email
- [ ] **Reliable Agency shared-login short usernames** — asked in 2026-04-22 email (proposed `reliable1@` / `reliable2@`)
- [ ] Will caregivers use ALIS on the shared phones (need ALIS accounts + Entra SSO) or only email?
- [ ] Does Cascades want to purchase 39 additional Business Premium licenses up-front, or roll out in waves (e.g., MedTechs first, then CCGs, then Caregivers)?
- [ ] Confirm pfSense WAN IP(s) are static enough to rely on in a CA Named Location policy

View File

@@ -1,29 +1,29 @@
# Follow-up email — staff list loose ends + access-policy decision
# Follow-up email — staff list loose ends (SENT)
**To:** Meredith Kuhn, John Trozzi (cc: Ashley Jensen)
**To:** Meredith Kuhn, John Trozzi, Ashley Jensen
**From:** Howard Enos — Computer Guru
**Date:** 2026-04-22
**Subject:** Got the staff list — thank you. A few follow-ups before I set up accounts.
**Status:** SENT
---
Meredith / John,
1. **Britney Thompson** — phone Y/N + outside sign-in Y/N
2. **Alma R Montt** — title / role. I see she is in Life Enrichment, is she an admin, manager, or something else?
3. **Polett Pinazavala** — MedTech - Memory Care (Tue-Sat) Desktop+phone, ALIS, NO outside access
4. **Ederick Yuzon** — first-name spelling
5. **Reliable Agency caregiver #1** — what sort name would you like them to have? reliable.agency.caregiver1 is long. Can do reliable1 and 2
6. **Reliable Agency caregiver #2**
Thank you for sending back the staff list — that's exactly what I needed and it's going straight into the account setup plan. Before I start creating M365 accounts and access policies, I want to confirm a few things so I don't make the wrong call:
1. **Ederick Yuzon** — just confirming the spelling of the first name. "Ederick" vs "Edrick" vs something else?
2. **Alma R Montt** — what's her actual title / role so I can put it on the account?
3. **Polett Pinazavala** — I had her on my caregiver roster (AM, Memory Care, MedTech) from earlier notes, and she's not on the returned list either. Did she leave?
4. **Access policy default:** I am going to set all staff restricted to signing in only from the building by default. It will be easier to set a list of users allowed to log in from outside the network than to try to lock individual people in one at a time.
Thank you —
Howard
I will send a full list for you all to look over.
---
*Edited by Howard from the earlier draft. Dropped questions that were resolved live (Kyla's username = `Kyla.QuickTiffany` per her own preference; Britney Thompson still employed; Christine Nyanzunda = one person / one account). Sent: TBD — Howard to update this doc with the actual sent copy.*
*Related: `reports/cascades-staff-2026-04-22.csv`, `docs/cloud/user-account-rollout-plan.md`.*
*This is the copy of the email Howard sent to Meredith / John / Ashley on 2026-04-22. Full working list (the referenced follow-up) is `cascades-staff-working-list-2026-04-22.md`.*
## Notes on what's being asked vs. declared
- **#1 Britney** — open question. Howard confirmed she's still employed; Meredith/John need to specify phone-yes-or-no and outside-sign-in-yes-or-no.
- **#2 Alma** — open question on role (admin / manager / other).
- **#3 Polett** — NOT a question. Howard is declaring the setup he intends: D+P, ALIS=Y, Outside=N, MedTech Memory Care TueSat. Meredith/John stop him if wrong.
- **#4 Ederick** — open question on first-name spelling.
- **#5 + #6 Agency** — open question on preferred short username. Howard's proposal: `reliable1` / `reliable2` rather than `reliable.agency.caregiver1/2`. These are treated as shared agency-login accounts rather than per-person accounts.

View File

@@ -0,0 +1,225 @@
# Cascades staff — working account setup list
**Prepared for:** Meredith Kuhn, John Trozzi, Ashley Jensen
**From:** Howard Enos — Computer Guru
**Date:** 2026-04-22
---
This is the working list I'm using to build every staff member's Microsoft 365 account, email, and access policy, based on the returned staff list. **Please skim and flag anything wrong, missing, or miscategorized.** Nothing is created yet — confirming this list is the go-ahead to start.
## How to read the columns
- **Email** — the M365 sign-in and email address the user will have. Existing AD accounts keep their current address; new accounts follow the `firstname.lastname@cascadestucson.com` pattern.
- **Phone (Y/N)** — Will this person use a Cascades-managed phone (shared handset or personal business phone) to sign into email / ALIS / Teams?
- **Outside sign-in (Y/N)** — **Default for everyone is NO** (building-only sign-in). A "Yes" here means the user is on the allow-list that permits signing in from home, personal cell, or off-site devices (with MFA and a managed device requirement). Call out anyone whose value here looks wrong.
Items marked **[?]** are the few things still needing a confirmation from you — see "Pending confirmations" at the bottom.
---
## Administrative
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Meredith Kuhn | Executive Director | meredith.kuhn@cascadestucson.com | Y | Y |
| Ashley Jensen | Assistant Executive Director | ashley.jensen@cascadestucson.com | Y | Y |
| Lauren Hasselman | Business Office Director | lauren.hasselman@cascadestucson.com | Y | Y |
| Allison Reibschied | Accounting Assistant | allison.reibschied@cascadestucson.com | Y | N |
## Marketing / Sales
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Megan Hiatt | Sales Director | megan.hiatt@cascadestucson.com | Y | Y |
| Crystal Rodriguez | Sales Associate | crystal.rodriguez@cascadestucson.com | Y | Y |
| Tamra Matthews | Move-In Coordinator *(leaving June 2026)* | tamra.matthews@cascadestucson.com | Y | Y |
## Assisted Living Nursing / Clinical
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Lois Lane | Health Services Director | lois.lane@cascadestucson.com | Y | Y |
| Karen Rossini | Health Services Manager | karen.rossini@cascadestucson.com | Y | Y |
| Veronica Feller | Care, AL Aide | veronica.feller@cascadestucson.com | Y | Y |
| Britney Thompson | Memory Care Nurse | britney.thompson@cascadestucson.com | Y *(confirm)* | Y *(confirm)* |
## Memory Care
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Shelby Trozzi | Memory Care Director | shelby.trozzi@cascadestucson.com | Y | Y |
| Christine Nyanzunda | MC Admin Assistant *(also part-time MedTech Sun/Mon — one account for both)* | christine.nyanzunda@cascadestucson.com | Y | Y |
## Resident Services
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Christina DuPras | Resident Services Director | christina.dupras@cascadestucson.com | Y | Y |
| Cathy Kingston | Receptionist *(shared front desk)* | cathy.kingston@cascadestucson.com | N | N |
| Shontiel Nunn | Receptionist *(shared front desk)* | shontiel.nunn@cascadestucson.com | N | N |
| Kyla QuickTiffany | Receptionist *(shared front desk)* | kyla.quicktiffany@cascadestucson.com | N | N |
| Michelle Shestko | MC Receptionist *(shared front desk)* | michelle.shestko@cascadestucson.com | N | N |
| Sebastian Leon | Courtesy Patrol | sebastian.leon@cascadestucson.com | Y | N |
| Sheldon Gardfrey | Courtesy Patrol | sheldon.gardfrey@cascadestucson.com | Y | N |
| Ray Rai | Courtesy Patrol | ray.rai@cascadestucson.com | Y | N |
## Life Enrichment
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Susan Hicks | Life Enrichment Director | susan.hicks@cascadestucson.com | Y | Y |
| Sharon Edwards | Life Enrichment Assistant | sharon.edwards@cascadestucson.com | Y | N |
| Alma R Montt | *(title TBD — see pending)* **[?]** | alma.montt@cascadestucson.com | Y | Y |
## Culinary
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| JD Martin | Culinary Director | jd.martin@cascadestucson.com | Y | Y |
| Ramon Castaneda | Kitchen Manager | ramon.castaneda@cascadestucson.com | Y | N |
| Alyssa Brooks | Dining Manager | alyssa.brooks@cascadestucson.com | Y | Y |
## Maintenance
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| John Trozzi | Facilities Director | john.trozzi@cascadestucson.com | Y | Y |
| Matt Brooks | Memory Care Receptionist / Maintenance *(dual-department)* | matt.brooks@cascadestucson.com | Y | N |
## Housekeeping
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Lupe Sanchez *(aka Guadalupe)* | Housekeeping Director | lupe.sanchez@cascadestucson.com | Y | Y |
## Transportation
| Name | Title | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Richard Adams | Driver | richard.adams@cascadestucson.com | Y *(phone only)* | N |
| Julian Crim | Driver | julian.crim@cascadestucson.com | Y *(phone only)* | N |
| Christopher Holick | Driver | christopher.holick@cascadestucson.com | Y *(phone only)* | N |
---
## Caregivers / Shift staff
All caregivers sign into the shared company-issued Android phones using their own account — each person needs their own M365 identity. Building-only sign-in for everyone in this group.
### TuesdaySaturday
| Name | Role | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Thelma Abainza | Caregiver — Tower | thelma.abainza@cascadestucson.com | Y | N |
| Niel Castro | MedTech / CCG — Tower | niel.castro@cascadestucson.com | Y | N |
| Espe Esperance | MedTech — Tower | espe.esperance@cascadestucson.com | Y | N |
| Barbara Johnson | Caregiver — Tower | barbara.johnson@cascadestucson.com | Y | N |
| Kasey Flores | Caregiver — Memory Care | kasey.flores@cascadestucson.com | Y | N |
| Richard Flores | Caregiver — Memory Care | richard.flores@cascadestucson.com | Y | N |
| Marie Kastner | Caregiver — Memory Care | marie.kastner@cascadestucson.com | Y | N |
| Bella Mendoza | Caregiver — Memory Care | bella.mendoza@cascadestucson.com | Y | N |
| Rosa Morales | MedTech — Memory Care | rosa.morales@cascadestucson.com | Y | N |
| Sandra Padilla | MedTech / CCG — Tower | sandra.padilla@cascadestucson.com | Y | N |
| Whisper Reed | MedTech — Tower *(overnight)* | whisper.reed@cascadestucson.com | Y | N |
| Patricia Sandoval-Beck | MedTech — Tower | patricia.sandoval-beck@cascadestucson.com | Y | N |
| Charity Sika | Caregiver — Memory Care | charity.sika@cascadestucson.com | Y | N |
| Ederick Yuzon **[?]** *(confirm spelling)* | Caregiver — Tower | ederick.yuzon@cascadestucson.com | Y | N |
### SundayThursday
| Name | Role | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Juan Andrade | Caregiver — Memory Care | juan.andrade@cascadestucson.com | Y | N |
| Jahmeka Clarke | MedTech — Memory Care | jahmeka.clarke@cascadestucson.com | Y | N |
| Karina Aziakpo | MedTech / CCG — MC overnight | karina.aziakpo@cascadestucson.com | Y | N |
| Jinnelle Dittbenner | Caregiver — Tower | jinnelle.dittbenner@cascadestucson.com | Y | N |
| Agnes McFerren | Caregiver — Tower | agnes.mcferren@cascadestucson.com | Y | N |
| Samuel Ramirez | Caregiver — Tower | samuel.ramirez@cascadestucson.com | Y | N |
| Erica Sanchez | Caregiver — Memory Care | erica.sanchez@cascadestucson.com | Y | N |
| Katrina Wyzykowski | MedTech — Memory Care | katrina.wyzykowski@cascadestucson.com | Y | N |
| Corey Tate | Caregiver — Tower NOC | corey.tate@cascadestucson.com | Y | N |
### FridayMonday / weekend
| Name | Role | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Ashli Atwood | MedTech / CCG — MC overnight | ashli.atwood@cascadestucson.com | Y | N |
| Cole Johnson | MedTech — Tower | cole.johnson@cascadestucson.com | Y | N |
| Roseline Cooper | Caregiver — MC overnight | roseline.cooper@cascadestucson.com | Y | N |
| Monique Lopez | Caregiver — Tower *(Fri+Sat doubles)* | monique.lopez@cascadestucson.com | Y | N |
| Gloria Williford | MedTech — MC *(Fri+Sat doubles)* | gloria.williford@cascadestucson.com | Y | N |
### ThursdayMonday / other
| Name | Role | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Sarah Carroll | Caregiver — Tower | sarah.carroll@cascadestucson.com | Y | N |
| Luke Hogan | Caregiver — Tower | luke.hogan@cascadestucson.com | Y | N |
| Gina Williams | Caregiver — Tower | gina.williams@cascadestucson.com | Y | N |
| Jen Higdon | Caregiver — Tower *(M/W/F AM)* | jen.higdon@cascadestucson.com | Y | N |
| Mary Kariuki | Caregiver — Tower *(SatMon + Wed PM)* | mary.kariuki@cascadestucson.com | Y | N |
| CeCe Lassey | Caregiver — Tower *(Sun/Mon doubles + Tue PM)* | cece.lassey@cascadestucson.com | Y | N |
| Patricia (Paty) Camarena Doran | MedTech / CCG — Tower *(Sun/Mon only)* | patricia.doran@cascadestucson.com | Y | N |
### PRN / part-time
| Name | Role | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Ezekiel Huerta | Caregiver PRN — Tower | ezekiel.huerta@cascadestucson.com | Y | N |
| Maia Baker | MedTech PRN — Memory Care | maia.baker@cascadestucson.com | Y | N |
| Polett Pinazavala | MedTech — Memory Care *(TueSat)* | polett.pinazavala@cascadestucson.com | Y | N |
### Agency — shared-account logins **[?]** *(confirm short username)*
These are shared logins used by whoever from Reliable Agency is covering a shift. Not tied to a specific person. Proposing short usernames rather than long `reliable.agency.caregiver1/2` — pending Meredith/John preference.
| Name | Role | Email | Phone | Outside sign-in |
|---|---|---|---|---|
| Reliable Agency shared login #1 | Agency caregiver | reliable1@cascadestucson.com *(proposed)* | Y | N |
| Reliable Agency shared login #2 | Agency caregiver | reliable2@cascadestucson.com *(proposed)* | Y | N |
---
## Summary counts
| Category | Count |
|---|---|
| Office staff with outside sign-in (Admin, Sales, Clinical incl. Britney, MC, RS Director, LE, Culinary, Maintenance, Housekeeping) | 19 |
| Office staff in-building only (Allison, Sharon, Ramon, Matt) | 4 |
| Shared front-desk receptionists | 4 |
| Courtesy Patrol | 3 |
| Drivers (phone-only) | 3 |
| Caregivers / shift staff (incl. Polett) | 38 |
| Agency shared logins | 2 |
| **Total identities to set up** | **73** |
## Pending confirmations (marked [?] above)
Asked in the 2026-04-22 email:
1. **Britney Thompson** — phone Y/N and outside sign-in Y/N? Existing account — just need the two flags.
2. **Alma R Montt** — title / role? (Admin, manager, something else in Life Enrichment?)
3. **Ederick Yuzon** — first-name spelling? (Ederick / Edrick / other?)
4. **Reliable Agency shared logins** — short username preference? Proposing `reliable1` and `reliable2` rather than the long `reliable.agency.caregiver1/2`.
**Declared (stop me if wrong):**
- Polett Pinazavala set up as MedTech — Memory Care (TueSat), Desktop+phone, ALIS, no outside sign-in.
## Update 2026-04-22 (post-email)
- Polett's row above is the declared setup — if Meredith/John don't push back, this goes live with the rest of the caregiver wave.
- Agency row emails are placeholders pending the short-username confirmation.
## Policy summary
- **Sign-in is locked to the Cascades building by default for every account above.**
- **Off-site sign-in is only allowed for the accounts marked "Outside sign-in = Y"**, and those accounts still require MFA and a managed device to sign in from off-site.
- Phones are provided for anyone marked "Phone = Y". Front-desk receptionists and drivers-without-a-phone are exceptions.
- Tamra Matthews' access will be removed in June 2026 when she leaves.
Please reply with corrections, or reply "all good" and I'll start building accounts.
Thank you —
Howard

View File

@@ -77,7 +77,7 @@ Allison + Sharon are borderline — ALIS handling alone doesn't mandate P2, but
**Note on Britney Thompson:** Previously predicted as a likely P2 candidate, absent from the 2026-04-22 CSV return. **Confirmed 2026-04-22 (Howard) — still an employee; needs Desktop + possibly Phone access.** Treated as Office-PHI (external-OK) clinical staff for license math until Meredith specifies a different posture. Add to purchase count.
**Note on Polett Pinazavala:** On the original 2026-04-18 caregiver roster, absent from the 2026-04-22 CSV return. **Confirmed 2026-04-22 (Howard) — still an employee; needs Desktop + possibly Phone.** Treated as Caregiver for license math (included in the caregiver rollout count, not in the office P2 count).
**Note on Polett Pinazavala:** On the original 2026-04-18 caregiver roster, absent from the 2026-04-22 CSV return. **Confirmed 2026-04-22 (Howard) — still employed.** Full setup declared in the 2026-04-22 email to Meredith/John: MedTech — Memory Care (TueSat), D+P, ALIS=Y, Outside=N. Counts as one Business Premium license in the caregiver total (not office P2).
**Shared-PC receptionists** (D only, no Outside, no ALIS): Cathy Kingston, Shontiel Nunn, Kyla Quick Tiffany, Michelle Shestko — four people on shared front-desk PCs. No individual P2 needed; their story is shared-account vs individual-account, not P2.
@@ -114,7 +114,7 @@ No answer yet. This decision directly changes the license count and the CA polic
- [x] ~~Follow up with John Trozzi on the gathering — he owes us the list~~ (received 2026-04-22 via CSV)
- [ ] Push Meredith for the "restrict everyone or just some" decision — still unanswered as of 2026-04-22
- [ ] Resolve open CSV questions (see `clients/cascades-tucson/docs/cloud/cascades-staff-followup-2026-04-22.md`): Kyla Quick Tiffany spelling, Ederick Yuzon spelling, Christine Nyanzunda caregiver-overlap, Alma R Montt title, Britney Thompson status
- [ ] Resolve remaining email questions (see `clients/cascades-tucson/docs/cloud/cascades-staff-followup-2026-04-22.md`): Britney phone+outside flags, Alma R Montt title, Ederick Yuzon spelling, agency shared-login username preference
- [ ] Decide: standalone P2 add-on for the 19 OR move those users to Business Premium OR move whole tenant to Business Premium (default recommendation: Premium tenant-wide)
- [ ] Build CA policy `CSC - Office Staff PHI Access` separate from the caregiver mobile policy
- [ ] Remember to REMOVE Tamra's license + CA exclusion on her departure date (June 2026 — confirmed)

View File

@@ -0,0 +1,735 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Cascades — Staff &amp; Department Editor (2026-04-22)</title>
<style>
:root {
--bg:#f4f6fa; --panel:#fff; --line:#d7dce4; --ink:#1a2030;
--muted:#5c6678; --accent:#2b6cb0; --accent-ink:#fff;
--caregiver:#fff6e5; --caregiver-line:#f1c27d;
--staff:#eef3fb; --staff-line:#c5d5ec;
--warn:#b42318; --ok:#15803d; --drop:#fff8c5;
--pending:#fdecec; --pending-line:#f4b4ae;
}
* { box-sizing:border-box; }
html,body { margin:0; background:var(--bg); color:var(--ink);
font:14px/1.4 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
header {
position:sticky; top:0; z-index:5; background:#1a2030; color:#fff;
padding:10px 16px; display:flex; gap:10px; align-items:center; flex-wrap:wrap;
box-shadow:0 1px 4px rgba(0,0,0,.15);
}
header h1 { font-size:16px; margin:0; flex:1; font-weight:600; }
header .sub { font-size:12px; color:#9aa4b8; margin-left:6px; }
button {
background:#2b6cb0; color:#fff; border:0; border-radius:5px;
padding:7px 12px; font:inherit; cursor:pointer;
}
button:hover { background:#224f84; }
button.ghost { background:transparent; color:#cfd6e4; border:1px solid #3a4458; }
button.ghost:hover { background:#2a3346; }
button.danger { background:#b42318; }
button.danger:hover { background:#8a1a11; }
main { padding:14px 16px 80px; max-width:1300px; margin:0 auto; }
.hint {
background:#fffbe6; border:1px solid #f1d982; color:#5c4a00;
padding:8px 12px; border-radius:6px; margin:0 0 14px; font-size:13px;
}
.hint b { color:#3d3200; }
.dept {
background:var(--panel); border:1px solid var(--line); border-radius:8px;
margin:10px 0; padding:0; overflow:hidden;
box-shadow:0 1px 3px rgba(0,0,0,.06);
}
.dept-header {
padding:10px 14px; background:#eef2f7; border-bottom:1px solid var(--line);
display:flex; gap:10px; align-items:center;
}
.dept-name { font-weight:600; font-size:15px; flex:1; outline:none; }
.dept-name:focus { background:#fff; padding:2px 6px; border-radius:4px; }
.count {
background:#fff; border:1px solid var(--line); color:var(--muted);
padding:2px 8px; border-radius:10px; font-size:12px;
}
.dept-actions { display:flex; gap:6px; }
.dept-actions button { padding:4px 8px; font-size:12px; }
.people { padding:6px 8px 10px; }
.person {
display:grid;
grid-template-columns: 18px 1.4fr 1.6fr auto auto auto auto auto auto;
gap:6px; align-items:center;
padding:6px 8px; margin:4px 0;
border:1px solid transparent; border-radius:5px;
background:var(--staff); border-color:var(--staff-line);
}
.person.caregiver { background:var(--caregiver); border-color:var(--caregiver-line); }
.person.pending { background:var(--pending); border-color:var(--pending-line); }
.person.dragging { opacity:.35; }
.person.drop-over { outline:2px dashed var(--accent); outline-offset:-2px; }
.grip {
cursor:grab; color:var(--muted); user-select:none; text-align:center;
font-size:16px; line-height:1;
}
.grip:active { cursor:grabbing; }
.field {
border:1px solid transparent; padding:4px 6px; border-radius:4px;
min-width:40px; outline:none;
}
.field:hover { border-color:#cfd6e4; background:#fff; }
.field:focus { border-color:var(--accent); background:#fff; }
.name { font-weight:600; }
.title { color:var(--muted); font-size:13px; }
.seg {
display:inline-flex; border:1px solid #c5cddb; border-radius:4px;
background:#fff; overflow:hidden; font-size:12px;
}
.seg label {
padding:3px 7px; cursor:pointer; user-select:none;
border-right:1px solid #e5e9f0;
}
.seg label:last-child { border-right:0; }
.seg input { display:none; }
.seg input:checked + span {
background:var(--accent); color:var(--accent-ink);
margin:-3px -7px; padding:3px 7px; display:inline-block;
}
.chk {
display:inline-flex; align-items:center; gap:4px;
font-size:12px; color:var(--muted); cursor:pointer; user-select:none;
padding:3px 6px; border:1px solid #c5cddb; border-radius:4px; background:#fff;
}
.chk input { accent-color:var(--accent); margin:0; }
.chk.on { color:var(--ok); border-color:#9ad6a4; background:#eefaf0; }
.dropdown-wrap { position:relative; }
select.dept-select {
font:inherit; font-size:12px; padding:4px 6px;
border:1px solid #c5cddb; border-radius:4px; background:#fff; max-width:150px;
}
.delete {
background:transparent; color:var(--warn); padding:3px 8px; font-size:16px;
line-height:1; border:1px solid transparent; border-radius:4px;
}
.delete:hover { background:#fde8e6; border-color:#f4b4ae; }
.notes {
grid-column: 2 / -1;
display:block; cursor:text;
margin-top:4px; padding:6px 8px;
border:1px dashed #cfd6e4; border-radius:4px;
background:rgba(255,255,255,.7);
font-size:12px; color:#2a3140; min-height:20px;
outline:none; white-space:pre-wrap; word-wrap:break-word;
}
.notes:hover { border-color:#9aa4b8; background:#fff; }
.notes:focus {
border-style:solid; border-color:var(--accent); background:#fff;
box-shadow:0 0 0 2px rgba(43,108,176,.15);
}
.notes:empty::before {
content: attr(data-placeholder);
color:#9aa4b8; font-style:italic;
}
.add-row {
padding:6px 8px; display:flex; gap:6px; align-items:center;
border-top:1px dashed #dbe0ea; margin-top:4px;
}
.add-row input {
flex:1; padding:5px 8px; border:1px solid #c5cddb; border-radius:4px;
font:inherit;
}
.add-row button { padding:5px 10px; font-size:13px; }
.drop-zone { min-height:24px; }
.drop-zone.drop-over {
background:var(--drop); border:2px dashed #caa300; border-radius:6px;
}
footer {
position:fixed; bottom:0; left:0; right:0;
background:#fff; border-top:1px solid var(--line);
padding:10px 16px; display:flex; gap:8px; align-items:center; flex-wrap:wrap;
box-shadow:0 -1px 3px rgba(0,0,0,.06);
}
footer .status { color:var(--muted); font-size:12px; flex:1; }
.legend { color:var(--muted); font-size:12px; }
.legend span { display:inline-block; width:10px; height:10px; border-radius:2px; vertical-align:middle; margin:0 4px 0 10px; }
.legend .l-staff { background:var(--staff); border:1px solid var(--staff-line); }
.legend .l-cg { background:var(--caregiver); border:1px solid var(--caregiver-line); }
.legend .l-pending { background:var(--pending); border:1px solid var(--pending-line); }
@media (max-width: 900px) {
.person {
grid-template-columns: 18px 1fr auto auto;
}
.person .title, .person .seg, .person .dropdown-wrap { grid-column: 2 / -1; }
}
@media print {
header, footer, .dept-actions, .add-row, .delete, .grip { display:none !important; }
.person { break-inside:avoid; background:#fff !important; border-color:#bbb !important; }
.dept { break-inside:avoid; page-break-inside:avoid; }
.dept-header { background:#eee !important; }
}
</style>
</head>
<body>
<header>
<h1>Cascades — Staff &amp; Department Editor <span class="sub">(revised 2026-04-22)</span></h1>
<span class="sub" id="savedTag">loaded</span>
<button onclick="exportJSON()" title="Download everything as JSON — email this file back to Howard">Export JSON</button>
<button class="ghost" onclick="importJSON()">Import JSON</button>
<button class="ghost" onclick="window.print()">Print</button>
<button class="danger" onclick="resetAll()" title="Revert to the Howard-provided baseline">Reset</button>
</header>
<main>
<div class="hint">
<b>What changed since last time:</b> the list now matches what you sent back. A few items are still marked in <b style="color:#b42318;">red</b> — please take a look and fill in the missing pieces (spelling, title, confirmations).
<ul style="margin:6px 0 0; padding-left:22px;">
<li><b>Drag</b> the <b>⋮⋮</b> grip on any name to move them to another department.</li>
<li><b>Click</b> any name or title to edit it inline. The notes box under each name is for anything you want to tell us.</li>
<li>
<b>Access type</b> (pick one):
<b>D</b> = desktop / PC only,
<b>P</b> = phone only,
<b>D+P</b> = both a desk and a phone,
<b></b> = not set.
</li>
<li><b>Outside</b> = this person is allowed to sign in from outside the building (home, personal cell, travel). <b>Default is OFF for everyone</b> — leave it off to lock to Cascades only, tick it only for people who truly need off-site access.</li>
<li><b>ALIS</b> = ticks if the person logs into ALIS.</li>
<li>Use the <b>+ Add</b> row at the bottom of any department to add anyone we've missed.</li>
<li>When you're done, click <b>Export JSON</b> and email the downloaded file to Howard. (JSON is the only format that imports back in, so it's the only export option.)</li>
</ul>
</div>
<div id="board"></div>
<div style="margin-top:18px; display:flex; gap:8px; align-items:center;">
<input id="newDept" placeholder="New department name (e.g. Security, Activities)" style="flex:1; padding:8px 10px; border:1px solid var(--line); border-radius:5px; font:inherit;">
<button onclick="addDept()">+ Add department</button>
</div>
</main>
<footer>
<span class="status" id="status">&nbsp;</span>
<span class="legend">
Legend: <span class="l-staff"></span>Staff <span class="l-cg"></span>Caregiver <span class="l-pending"></span>Needs answer
</span>
</footer>
<script>
// -----------------------------------------------------------------------------
// Baseline roster — 2026-04-22, after merging the returned CSV + Howard's
// live confirmations. Items flagged "pending" have notes starting with "[?]"
// which colors the row pink so Meredith/John can't miss them.
// -----------------------------------------------------------------------------
const INITIAL = {
departments: [
"Administrative","Marketing / Sales",
"Care, Assisted Living (Nursing / Clinical)","Care, Memory Care",
"Resident Services","Life Enrichment","Culinary","Maintenance",
"Housekeeping","Transportation","Caregivers (shift staff)"
],
people: [
// Administrative
["Meredith Kuhn","Executive Director","Administrative","D+P",true,true,""],
["Ashley Jensen","Assistant Executive Director","Administrative","D+P",true,true,""],
["Lauren Hasselman","Business Office Director","Administrative","D+P",true,true,""],
["Allison Reibschied","Accounting Assistant","Administrative","D+P",false,true,""],
// Marketing / Sales
["Megan Hiatt","Sales Director","Marketing / Sales","D+P",true,true,"Handles resident intake (PHI)"],
["Crystal Rodriguez","Sales Associate","Marketing / Sales","D+P",true,true,"Handles resident intake (PHI)"],
["Tamra Matthews","Move-In Coordinator","Marketing / Sales","D+P",true,true,"Leaving June 2026 — confirmed"],
// Care, Assisted Living (Nursing / Clinical)
["Lois Lane","Health Services Director","Care, Assisted Living (Nursing / Clinical)","D+P",true,true,""],
["Karen Rossini","Health Services Manager","Care, Assisted Living (Nursing / Clinical)","D+P",true,true,""],
["Veronica Feller","Care, Assisted Living Aide","Care, Assisted Living (Nursing / Clinical)","D+P",true,true,""],
["Britney Thompson","Memory Care Nurse","Care, Assisted Living (Nursing / Clinical)","D+P",true,true,"[?] Phone Y/N and outside sign-in Y/N? Currently assumed D+P with outside sign-in — flip the flags if that's wrong."],
// Care, Memory Care
["Shelby Trozzi","Memory Care Director","Care, Memory Care","D+P",true,true,""],
["Christine Nyanzunda","Memory Care Admin Assistant","Care, Memory Care","D+P",true,true,"Dual role — MC Admin + part-time Sun/Mon MedTech — one account covers both (confirmed)"],
// Resident Services
["Christina DuPras","Resident Services Director","Resident Services","D+P",true,true,""],
["Cathy Kingston","Receptionist","Resident Services","D",false,false,"Front desk shared PC"],
["Shontiel Nunn","Receptionist","Resident Services","D",false,false,"Front desk shared PC"],
["Kyla QuickTiffany","Receptionist","Resident Services","D",false,false,"Shared front desk. Surname spelled as one word per her preference — account will be kyla.quicktiffany@"],
["Michelle Shestko","MC Receptionist","Resident Services","D",false,false,"MC front desk shared PC"],
["Sebastian Leon","Courtesy Patrol","Resident Services","D+P",false,false,""],
["Sheldon Gardfrey","Courtesy Patrol","Resident Services","D+P",false,false,""],
["Ray Rai","Courtesy Patrol","Resident Services","D+P",false,false,""],
// Life Enrichment
["Susan Hicks","Life Enrichment Director","Life Enrichment","D+P",true,true,""],
["Sharon Edwards","Life Enrichment Assistant","Life Enrichment","D+P",false,true,""],
["Alma R Montt","","Life Enrichment","D+P",true,true,"[?] What's her title / role — admin, manager, or something else in Life Enrichment? Please type it into the Title box."],
// Culinary
["JD Martin","Culinary Director","Culinary","D+P",true,true,""],
["Ramon Castaneda","Kitchen Manager","Culinary","D+P",false,false,""],
["Alyssa Brooks","Dining Manager","Culinary","D+P",true,true,""],
// Maintenance
["John Trozzi","Facilities Director","Maintenance","D+P",true,true,""],
["Matt Brooks","Memory Care Receptionist / Maintenance","Maintenance","D+P",false,true,"Works in both Maintenance and MC — confirmed"],
// Housekeeping
["Lupe Sanchez","Housekeeping Director","Housekeeping","D+P",true,true,"AKA Guadalupe Sanchez"],
// Transportation
["Richard Adams","Driver","Transportation","P",false,false,"Phone only"],
["Julian Crim","Driver","Transportation","P",false,false,"Phone only"],
["Christopher Holick","Driver","Transportation","P",false,false,"Phone only"],
// Caregivers (shift staff) — TueSat
["Thelma Abainza","Caregiver — Tower (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Niel Castro","MedTech / CCG — Tower (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Espe Esperance","MedTech — Tower (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Barbara Johnson","Caregiver — Tower (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Kasey Flores","Caregiver — Memory Care (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Richard Flores","Caregiver — Memory Care (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Marie Kastner","Caregiver — Memory Care (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Bella Mendoza","Caregiver — Memory Care (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Rosa Morales","MedTech — Memory Care (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Sandra Padilla","MedTech / CCG — Tower (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Polett Pinazavala","MedTech — Memory Care (TueSat)","Caregivers (shift staff)","D+P",false,true,"Confirmed still employed 2026-04-22. Setup declared: D+P, ALIS, no outside sign-in."],
["Whisper Reed","MedTech — Tower overnight (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Patricia Sandoval-Beck","MedTech — Tower (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Charity Sika","Caregiver — Memory Care (TueSat)","Caregivers (shift staff)","D+P",false,true,""],
["Ederick Yuzon","Caregiver — Tower (TueSat)","Caregivers (shift staff)","D+P",false,true,"[?] Confirm spelling of the first name — Ederick vs Edrick vs other?"],
// Caregivers — SunThu
["Juan Andrade","Caregiver — Memory Care (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
["Jahmeka Clarke","MedTech — Memory Care (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
["Karina Aziakpo","MedTech / CCG — MC overnight (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
["Jinnelle Dittbenner","Caregiver — Tower (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
["Agnes McFerren","Caregiver — Tower (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
["Samuel Ramirez","Caregiver — Tower (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
["Erica Sanchez","Caregiver — Memory Care (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
["Katrina Wyzykowski","MedTech — Memory Care (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
["Corey Tate","Caregiver — Tower NOC (SunThu)","Caregivers (shift staff)","D+P",false,true,""],
// Caregivers — FriMon / weekend doubles
["Ashli Atwood","MedTech / CCG — MC overnight (FriMon)","Caregivers (shift staff)","D+P",false,true,""],
["Cole Johnson","MedTech — Tower (FriMon)","Caregivers (shift staff)","D+P",false,true,""],
["Roseline Cooper","Caregiver — MC overnight (FriMon)","Caregivers (shift staff)","D+P",false,true,""],
["Monique Lopez","Caregiver — Tower Fri+Sat doubles","Caregivers (shift staff)","D+P",false,true,""],
["Gloria Williford","MedTech — MC Fri+Sat doubles","Caregivers (shift staff)","D+P",false,true,""],
// Caregivers — ThuMon
["Sarah Carroll","Caregiver — Tower (ThuMon)","Caregivers (shift staff)","D+P",false,true,""],
["Luke Hogan","Caregiver — Tower (ThuMon)","Caregivers (shift staff)","D+P",false,true,""],
["Gina Williams","Caregiver — Tower (ThuMon)","Caregivers (shift staff)","D+P",false,true,""],
// Caregivers — other patterns
["Jen Higdon","Caregiver — Tower M/W/F AM","Caregivers (shift staff)","D+P",false,true,""],
["Mary Kariuki","Caregiver — Tower SatMon + Wed PM","Caregivers (shift staff)","D+P",false,true,""],
["CeCe Lassey","Caregiver — Tower Sun/Mon doubles + Tue PM","Caregivers (shift staff)","D+P",false,true,""],
["Patricia Camarena Doran","MedTech / CCG — Tower Sun/Mon only","Caregivers (shift staff)","D+P",false,true,"Also goes by Paty / Patti — legal name confirmed. Account will be patricia.doran@"],
// Caregivers — PRN / part-time
["Ezekiel Huerta","Caregiver PRN — Tower","Caregivers (shift staff)","D+P",false,true,""],
["Maia Baker","MedTech PRN — Memory Care","Caregivers (shift staff)","D+P",false,true,"Part-time (confirmed)"],
// Caregivers — agency shared-login accounts
["Reliable Agency shared login #1","Agency caregiver","Caregivers (shift staff)","D+P",false,true,"[?] Shared login (not per-person — whoever from Reliable is on shift signs in). Please confirm short username preference: proposing reliable1@ rather than the long reliable.agency.caregiver1@."],
["Reliable Agency shared login #2","Agency caregiver","Caregivers (shift staff)","D+P",false,true,"[?] Shared login (not per-person — whoever from Reliable is on shift signs in). Please confirm short username preference: proposing reliable2@ rather than the long reliable.agency.caregiver2@."],
]
};
const CAREGIVER_DEPT = "Caregivers (shift staff)";
const STORAGE_KEY = "cascades-staff-editor-2026-04-22";
// -----------------------------------------------------------------------------
// State
// -----------------------------------------------------------------------------
let state = pickInitialState();
function pickInitialState() {
const local = load();
return local || rebuild(INITIAL);
}
function rebuild(src) {
let pid = 1;
return {
departments: [...src.departments],
people: src.people.map(p => ({
id: pid++, name: p[0], title: p[1], dept: p[2],
access: p[3], outside: !!p[4], alis: !!p[5], notes: p[6] || ""
})),
savedAt: new Date().toISOString()
};
}
function save() {
state.savedAt = new Date().toISOString();
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
document.getElementById("savedTag").textContent = "saved " + new Date().toLocaleTimeString();
} catch(e) { document.getElementById("savedTag").textContent = "save failed"; }
}
function load() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const obj = JSON.parse(raw);
if (!obj.departments || !obj.people) return null;
return obj;
} catch(e) { return null; }
}
function resetAll() {
if (!confirm("Reset everything back to the Howard-provided 2026-04-22 baseline? Your edits will be lost.")) return;
state = rebuild(INITIAL);
save(); render();
}
// -----------------------------------------------------------------------------
// Rendering
// -----------------------------------------------------------------------------
function render() {
const board = document.getElementById("board");
board.innerHTML = "";
const deptSet = new Set(state.departments);
for (const p of state.people) {
if (!deptSet.has(p.dept)) {
if (!deptSet.has("Unsorted")) { state.departments.push("Unsorted"); deptSet.add("Unsorted"); }
p.dept = "Unsorted";
}
}
for (const dept of state.departments) {
const peopleInDept = state.people.filter(p => p.dept === dept);
const det = document.createElement("div");
det.className = "dept"; det.dataset.dept = dept;
const sum = document.createElement("div");
sum.className = "dept-header";
sum.innerHTML = `
<span class="dept-name" contenteditable spellcheck="false"
onblur="renameDept(this, '${escapeAttr(dept)}')"
onkeydown="if(event.key==='Enter'){event.preventDefault();this.blur();}">${escapeHTML(dept)}</span>
<span class="count">${peopleInDept.length}</span>
<span class="dept-actions">
<button onclick="moveDeptUp('${escapeAttr(dept)}')" title="Move up">↑</button>
<button onclick="moveDeptDown('${escapeAttr(dept)}')" title="Move down">↓</button>
<button class="danger" onclick="deleteDept('${escapeAttr(dept)}')" title="Delete department (moves members to Unsorted)">✕</button>
</span>`;
det.appendChild(sum);
const list = document.createElement("div");
list.className = "people drop-zone";
list.dataset.dept = dept;
list.addEventListener("dragover", onDragOver);
list.addEventListener("dragleave", onDragLeave);
list.addEventListener("drop", onDrop);
sum.addEventListener("dragover", onDragOver);
sum.addEventListener("dragleave", onDragLeave);
sum.addEventListener("drop", onDrop);
for (const p of peopleInDept) list.appendChild(personRow(p));
const add = document.createElement("div");
add.className = "add-row";
add.innerHTML = `
<input placeholder="New person — name" data-field="name">
<input placeholder="Title / role" data-field="title" style="flex:0.8;">
<button>+ Add</button>`;
const btn = add.querySelector("button");
btn.onclick = () => {
const name = add.querySelector("[data-field=name]").value.trim();
const title = add.querySelector("[data-field=title]").value.trim();
if (!name) return;
state.people.push({
id: nextId(), name, title, dept,
access: "D+P",
outside: false, alis: false, notes: ""
});
save(); render();
};
list.appendChild(add);
det.appendChild(list);
board.appendChild(det);
}
updateStatus();
}
function personRow(p) {
const row = document.createElement("div");
const isPending = (p.notes || "").trim().startsWith("[?]");
row.className = "person"
+ (p.dept === CAREGIVER_DEPT ? " caregiver" : "")
+ (isPending ? " pending" : "");
row.dataset.id = p.id;
row.innerHTML = `
<span class="grip" draggable="true" title="Drag to move this person to another department">⋮⋮</span>
<span class="field name" contenteditable spellcheck="false"
data-id="${p.id}" data-k="name">${escapeHTML(p.name)}</span>
<span class="field title" contenteditable spellcheck="false"
data-id="${p.id}" data-k="title">${escapeHTML(p.title)}</span>
<span class="seg" title="Access type">
${accessSeg(p)}
</span>
<label class="chk ${p.outside ? "on" : ""}" title="Allowed to sign in outside the building">
<input type="checkbox" ${p.outside ? "checked" : ""} data-id="${p.id}" data-k="outside">Outside
</label>
<label class="chk ${p.alis ? "on" : ""}" title="Uses ALIS">
<input type="checkbox" ${p.alis ? "checked" : ""} data-id="${p.id}" data-k="alis">ALIS
</label>
<span class="dropdown-wrap">
<select class="dept-select" data-id="${p.id}" title="Move to another department">
${state.departments.map(d => `<option ${d===p.dept?"selected":""}>${escapeHTML(d)}</option>`).join("")}
</select>
</span>
<button class="delete" title="Remove this person" data-id="${p.id}">✕</button>
<span class="notes" contenteditable spellcheck="true"
data-id="${p.id}" data-k="notes"
data-placeholder="Click to add notes about this person — spelling, department conflicts, schedule, etc.">${escapeHTML(p.notes)}</span>
`;
row.querySelectorAll("[contenteditable]").forEach(el => {
el.addEventListener("blur", onFieldBlur);
el.addEventListener("keydown", e => {
if (e.key === "Enter" && !el.classList.contains("notes")) {
e.preventDefault(); el.blur();
}
});
});
row.querySelectorAll(".notes").forEach(el => {
el.addEventListener("mousedown", onNotesMouseDown);
});
row.querySelectorAll('input[type="radio"]').forEach(el => el.addEventListener("change", onAccessChange));
row.querySelectorAll('input[type="checkbox"]').forEach(el => el.addEventListener("change", onToggle));
row.querySelector("select.dept-select").addEventListener("change", onDeptSelect);
row.querySelector("button.delete").addEventListener("click", onDelete);
const grip = row.querySelector(".grip");
grip.addEventListener("dragstart", onDragStart);
grip.addEventListener("dragend", onDragEnd);
return row;
}
function accessSeg(p) {
const opts = [
{v:"D", label:"D", title:"Desktop / PC only"},
{v:"P", label:"P", title:"Phone only"},
{v:"D+P", label:"D+P", title:"Both desktop and phone"},
{v:"", label:"—", title:"Not set / unknown"}
];
return opts.map(o => `
<label title="${o.title}">
<input type="radio" name="acc-${p.id}" value="${o.v}" ${p.access===o.v?"checked":""} data-id="${p.id}" data-k="access">
<span>${o.label}</span>
</label>`).join("");
}
function updateStatus() {
const total = state.people.length;
const staff = state.people.filter(p => p.dept !== CAREGIVER_DEPT).length;
const cg = total - staff;
const outside = state.people.filter(p => p.outside).length;
const alis = state.people.filter(p => p.alis).length;
const pending = state.people.filter(p => (p.notes || "").trim().startsWith("[?]")).length;
const pendingText = pending ? ` · ${pending} still need answer` : "";
document.getElementById("status").textContent =
`${total} people (${staff} staff + ${cg} caregiver) · ${outside} outside-access · ${alis} ALIS · ${state.departments.length} departments${pendingText}`;
}
// -----------------------------------------------------------------------------
// Field handlers
// -----------------------------------------------------------------------------
function onFieldBlur(e) {
const id = +e.target.dataset.id, k = e.target.dataset.k;
const p = state.people.find(x => x.id === id); if (!p) return;
const v = e.target.textContent.trim();
if (p[k] !== v) {
p[k] = v;
save();
if (k === "notes") render(); else updateStatus();
}
}
function onAccessChange(e) {
const id = +e.target.dataset.id;
const p = state.people.find(x => x.id === id); if (!p) return;
p.access = e.target.value; save();
}
function onToggle(e) {
const id = +e.target.dataset.id, k = e.target.dataset.k;
const p = state.people.find(x => x.id === id); if (!p) return;
p[k] = e.target.checked;
e.target.closest("label").classList.toggle("on", e.target.checked);
save(); updateStatus();
}
function onDeptSelect(e) {
const id = +e.target.dataset.id;
const p = state.people.find(x => x.id === id); if (!p) return;
p.dept = e.target.value; save(); render();
}
function onDelete(e) {
const id = +e.currentTarget.dataset.id;
const p = state.people.find(x => x.id === id); if (!p) return;
if (!confirm(`Remove ${p.name} from the list? (You can add them back later.)`)) return;
state.people = state.people.filter(x => x.id !== id);
save(); render();
}
function renameDept(el, oldName) {
const newName = el.textContent.trim();
if (!newName || newName === oldName) { el.textContent = oldName; return; }
if (state.departments.includes(newName)) {
alert("A department with that name already exists."); el.textContent = oldName; return;
}
const i = state.departments.indexOf(oldName);
if (i >= 0) state.departments[i] = newName;
for (const p of state.people) if (p.dept === oldName) p.dept = newName;
save(); render();
}
function moveDeptUp(name) {
const i = state.departments.indexOf(name); if (i <= 0) return;
[state.departments[i-1], state.departments[i]] = [state.departments[i], state.departments[i-1]];
save(); render();
}
function moveDeptDown(name) {
const i = state.departments.indexOf(name); if (i < 0 || i >= state.departments.length-1) return;
[state.departments[i+1], state.departments[i]] = [state.departments[i], state.departments[i+1]];
save(); render();
}
function deleteDept(name) {
const n = state.people.filter(p => p.dept === name).length;
if (!confirm(`Delete department "${name}"?` + (n ? ` ${n} person(s) will move to Unsorted.` : ""))) return;
state.departments = state.departments.filter(d => d !== name);
for (const p of state.people) if (p.dept === name) p.dept = "Unsorted";
if (state.people.some(p => p.dept === "Unsorted") && !state.departments.includes("Unsorted"))
state.departments.push("Unsorted");
save(); render();
}
function addDept() {
const el = document.getElementById("newDept");
const name = el.value.trim();
if (!name) return;
if (state.departments.includes(name)) { alert("That department already exists."); return; }
state.departments.push(name); el.value = "";
save(); render();
}
// -----------------------------------------------------------------------------
// Drag & drop
// -----------------------------------------------------------------------------
let dragId = null;
function onDragStart(e) {
const row = e.currentTarget.closest(".person");
if (!row) return;
dragId = +row.dataset.id;
row.classList.add("dragging");
e.dataTransfer.effectAllowed = "move";
try { e.dataTransfer.setData("text/plain", String(dragId)); } catch(_) {}
try { e.dataTransfer.setDragImage(row, 20, 10); } catch(_) {}
}
function onDragEnd(e) {
const row = e.currentTarget.closest(".person");
if (row) row.classList.remove("dragging");
document.querySelectorAll(".drop-over").forEach(n => n.classList.remove("drop-over"));
dragId = null;
}
function onDragOver(e) {
e.preventDefault();
const zone = e.currentTarget;
zone.classList.add("drop-over");
e.dataTransfer.dropEffect = "move";
}
function onDragLeave(e) {
e.currentTarget.classList.remove("drop-over");
}
function onDrop(e) {
e.preventDefault();
const zone = e.currentTarget;
zone.classList.remove("drop-over");
const dept = zone.dataset.dept || zone.closest(".dept")?.dataset.dept;
if (!dept) return;
const id = dragId || +e.dataTransfer.getData("text/plain");
const p = state.people.find(x => x.id === id); if (!p) return;
if (p.dept === dept) return;
p.dept = dept; save(); render();
}
// -----------------------------------------------------------------------------
// Export / import — JSON only (it's the only format we can import back in)
// -----------------------------------------------------------------------------
function exportJSON() {
const data = JSON.stringify(state, null, 2);
downloadBlob(`cascades-staff-${stamp()}.json`, data, "application/json");
}
function importJSON() {
const inp = document.createElement("input");
inp.type = "file"; inp.accept = ".json,application/json";
inp.onchange = () => {
const f = inp.files[0]; if (!f) return;
const r = new FileReader();
r.onload = () => {
try {
const obj = JSON.parse(r.result);
if (!obj.departments || !obj.people) throw new Error("Missing fields");
state = obj; save(); render();
alert("Imported OK.");
} catch(e) { alert("Could not read that file: " + e.message); }
};
r.readAsText(f);
};
inp.click();
}
function downloadBlob(name, data, mime) {
const blob = new Blob([data], {type: mime});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob); a.download = name;
document.body.appendChild(a); a.click();
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 0);
}
function stamp() {
const d = new Date(), p = n => String(n).padStart(2,"0");
return `${d.getFullYear()}-${p(d.getMonth()+1)}-${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}`;
}
// -----------------------------------------------------------------------------
// Utilities
// -----------------------------------------------------------------------------
function onNotesMouseDown(e) {
const el = e.currentTarget;
let inside = false;
if (document.caretPositionFromPoint) {
const p = document.caretPositionFromPoint(e.clientX, e.clientY);
if (p && el.contains(p.offsetNode)) inside = true;
} else if (document.caretRangeFromPoint) {
const r = document.caretRangeFromPoint(e.clientX, e.clientY);
if (r && el.contains(r.startContainer)) inside = true;
}
if (!inside) {
e.preventDefault();
el.focus();
const r = document.createRange();
r.selectNodeContents(el);
r.collapse(false);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(r);
}
}
function nextId() { return Math.max(0, ...state.people.map(p => p.id)) + 1; }
function escapeHTML(s) { return String(s ?? "").replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
function escapeAttr(s) { return String(s ?? "").replace(/'/g, "\\'"); }
render();
</script>
</body>
</html>

View File

@@ -119,14 +119,14 @@ These must be resolved before creating or converting accounts. See also `cascade
| Discrepancy | Status | Action |
|---|---|---|
| **Britney Thompson** — in AD (enabled, Memory Care Nurse), NOT on returned CSV | **Resolved 2026-04-22 (Howard) — still employed. Desktop + maybe Phone.** | Keep existing AD account. Treat as Office-PHI / clinical (D+P, ALIS=Y). Confirm phone tier and Outside posture with Meredith. |
| **Polett Pinazavala** — on 2026-04-18 caregiver roster, NOT on returned CSV | **Resolved 2026-04-22 (Howard) — still employed. Desktop + maybe Phone.** | Keep on caregiver roster. Include in Wave 3 caregiver account creation. Confirm phone tier with Meredith. |
| **Polett Pinazavala** — on 2026-04-18 caregiver roster, NOT on returned CSV | **Resolved 2026-04-22 (Howard) — still employed. Setup declared in email: MedTech — Memory Care (TueSat), D+P, ALIS=Y, Outside=N.** | Keep on caregiver roster. Include in Wave 3 caregiver account creation unless Meredith/John push back on the declared setup. |
| **Christine Nyanzunda** — one person, MC Admin + part-time Sun/Mon MedTech | **Resolved 2026-04-22 (Howard) — one account covers both roles.** | Single account in `OU=Care-MemoryCare`. Default building-only CA policy. When she's covering a MedTech shift she logs into the shared MC phone with her own account. If that sign-in gets blocked by the shared-phone CA, add her to a specific exception group rather than splitting into two accounts. |
| **Alma R Montt** — on CSV (Life Enrichment), NOT in AD, title blank | **Username assigned 2026-04-22 (Howard): `Alma.Montt`.** Title still pending Meredith. | Create AD account at `Alma.Montt` (UPN `alma.montt@cascadestucson.com`). Populate title once Meredith answers. |
| **Kyla Quick Tiffany** — on CSV and in AD "needs account" list | **Username assigned 2026-04-22 (Howard, per Kyla's preference): `Kyla.QuickTiffany`** — last name treated as a single word. | Create AD account at `Kyla.QuickTiffany` (UPN `kyla.quicktiffany@cascadestucson.com`). Persona: Shared-PC Reception. |
| **Ederick Yuzon** — spelling not confirmed | Still pending Meredith. | Block on creation; use `Ederick.Yuzon` tentatively if Meredith confirms. |
| **Matt Brooks** — AD dept = Maintenance, CSV note "works in both departments" | Confirmed (CSV-inline). | Keep in Maintenance OU; add to secondary MC group for access overlap. |
| **37 caregivers** — on CSV, none in AD | Unchanged. | Create all 37 AD accounts (+ M365) in Wave 3. |
| **2 agency placeholders** — on CSV, not in AD | Unchanged. | Decide with Meredith: real accounts or ALIS-only? |
| **2 agency placeholders** — on CSV, not in AD | **Decision 2026-04-22 (Howard, asked Meredith for username preference):** shared agency-login accounts, not per-person. Proposed usernames `reliable1@` and `reliable2@` (fallback to `reliable.agency.caregiver1/2@` if Meredith prefers long form). | Create 2 shared AD/M365 accounts once username preference comes back. Shared accounts = caveat on audit attribution — whoever is on shift uses the shared login, so individual accountability in audit logs is weaker for agency staff. Acceptable tradeoff given we don't have agency-staff names. |
| **Generic AD accounts** (`Culinary`, `RECEPTIONIST`, `saleshare`, `directoryshare`) | Unchanged. | Phase 5 cleanup after named-account coverage. |
**Username convention for new accounts:** TitleCase `First.Last` (e.g., `Alma.Montt`, `Kyla.QuickTiffany`). Existing lowercase exceptions in AD (`britney.thompson`, `karen.rossini`, `lauren.hasselman`) are the known legacy cases — leave as-is, don't rename. All net-new accounts follow TitleCase.
@@ -134,15 +134,14 @@ These must be resolved before creating or converting accounts. See also `cascade
## 7. Rollout sequence
### Wave 0 — Pre-flight (blocks waves 1+)
- Get answers to the 5 follow-up questions (Kyla/Ederick/Christine/Alma/Britney) + the "restrict-everyone or selective" policy decision from Meredith
- Close Polett Pinazavala discrepancy
- Final license decision (Business Premium tenant-wide vs. mixed)
- Get answers to the 4 remaining email questions: Britney flags, Alma title, Ederick spelling, agency short-username preference
- Final license decision (Business Premium tenant-wide vs. mixed) — recommendation is Premium tenant-wide, needs Meredith sign-off
- Purchase license count locked in
### Wave 1 — New office accounts (low blast radius)
- Create AD + M365 for Alma R Montt and Kyla Quick Tiffany (the only new office/reception accounts the CSV produces)
- Create AD + M365 for Alma R Montt (`Alma.Montt`) and Kyla QuickTiffany (`Kyla.QuickTiffany`) — the only new office/reception accounts the CSV produces
- Validate group membership + CA policy assignment on these two before touching anyone else
- Pilot the `CSC - FrontDesk Building-Only` policy with Kyla
- Pilot the `CSC - Building Only (Default)` policy with Kyla
### Wave 2 — Existing office accounts, reassignment only
- Move existing users into new OU layout (no identity changes, just OU move + group membership)
@@ -155,7 +154,7 @@ These must be resolved before creating or converting accounts. See also `cascade
### Wave 4 — Cleanup
- Disable/remove `Culinary`, `RECEPTIONIST`, `saleshare`, `directoryshare` generics once their functions are covered by named accounts + shared mailboxes
- Disable departed accounts (Britney pending answer, Tamra on departure June 2026)
- Disable Tamra's account on her June 2026 departure (other known departures: none as of 2026-04-22)
- Rotate `krbtgt` password (noted stale in AD doc — overdue)
## 8. Account creation template (per new user)
@@ -182,20 +181,20 @@ Applies to Wave 1 + Wave 3 (and any future hire). Precise script will be built l
## 10. Open decisions blocking the rollout
1. **Business Premium tenant-wide vs. mixed SKUs** — Meredith, tied to the upgrade proposal. Building-only-by-default decision reinforces Premium tenant-wide (see §5).
2. **Ederick Yuzon spelling** — Meredith/John, in the 2026-04-22 follow-up email.
3. **Alma R Montt title** — Meredith/John, in the follow-up email.
4. **Britney phone + Outside posture** — Meredith (employment confirmed by Howard; access tier still TBD).
5. **Polett employment confirmation** — Meredith (Howard assumes still employed; formal Meredith confirmation requested in follow-up email).
6. **Agency placeholder accounts — names + ALIS-only vs. real accounts?** — John added two agency rows to the CSV but left Name and Notes blank. Need the actual agency names + whether they need AD/M365 identities or just ALIS web logins.
7. **Drivers: F3 or Business Standard?** — Meredith (cost vs. Office install need). Note: drivers need allow-list membership to sign in off-site, so whichever tier must include P1 for CA coverage (F3 does not; Business Premium or Business Standard + Entra P1 add-on required).
2. **Britney Thompson phone Y/N + Outside Y/N** — Meredith/John, asked in the 2026-04-22 email.
3. **Alma R Montt title** — Meredith/John, asked in the 2026-04-22 email (admin / manager / other in LE?).
4. **Ederick Yuzon spelling** — Meredith/John, asked in the 2026-04-22 email.
5. **Agency shared-login short username** — Meredith/John, asked in the 2026-04-22 email (proposed `reliable1` / `reliable2`).
6. **Drivers: F3 or Business Standard?** — Meredith (cost vs. Office install need). Drivers need allow-list membership to sign in off-site, so whichever tier must include P1 for CA coverage (F3 does not; Business Premium or Business Standard + Entra P1 add-on required).
**Resolved 2026-04-22 (Howard):**
- Restrict-everyone default vs. selective → **building-only by default, allow-list for exceptions** (§5).
- Christine Nyanzunda → one account covers both roles.
- Kyla Quick Tiffany username → `Kyla.QuickTiffany` (her preference — sign-in confirmed by Howard).
- Kyla Quick Tiffany username → `Kyla.QuickTiffany` (her preference — confirmed by Howard).
- Alma R Montt username → `Alma.Montt`.
- Britney Thompson → still employed; stays in AD.
- Polett Pinazavala → still employed (awaiting Meredith formal confirmation via email).
- Britney Thompson → still employed; stays in AD. Access-posture flags still open.
- Polett Pinazavala → still employed. Declared setup (D+P, ALIS=Y, Outside=N, MedTech Memory Care TueSat) announced in 2026-04-22 email; will proceed unless Meredith/John push back.
- Agency placeholders → shared-login accounts (not per-person). Username short-form proposed but preference still open.
## 11. Related docs

View File

@@ -0,0 +1,267 @@
"""Build a minimal Word document listing the six people with open questions.
No external deps — a .docx is a zip of OOXML files, so we construct it directly
with the stdlib. Output: cascades-staff-open-questions-2026-04-22.docx
"""
from __future__ import annotations
import os
import zipfile
from xml.sax.saxutils import escape
OUT = "clients/cascades-tucson/docs/cloud/questionnaires/cascades-staff-open-questions-2026-04-22.docx"
TITLE = "Cascades — Open Items on Staff Access List"
SUBTITLE = "2026-04-22 · prepared by Howard Enos, Computer Guru · matches the 2026-04-22 email"
INTRO = (
"Thank you for sending back the staff list. Almost everything is squared away. "
"Below are the few items I still need from you. One of them (Polett) is NOT a "
"question — it is the setup I am planning to use; stop me if it's wrong. The rest "
"are questions. Short answers are fine. I will send a full list for you all to "
"look over separately."
)
QUESTIONS = [
{
"name": "Britney Thompson",
"dept": "Assisted Living Nursing / Clinical",
"context": (
"Britney has an active Active Directory account today as Memory Care Nurse. "
"She was not on the staff list you returned; Howard has confirmed she is still "
"an employee, so the account stays active. I just need the two flags below."
),
"questions": [
"Phone — Y or N? (Does she need a Cascades-issued phone / business cell, in addition to a desktop?)",
"Outside sign-in — Y or N? (Default for everyone is N / building-only. Mark Y only if she legitimately works off-site.)",
],
},
{
"name": "Alma R Montt",
"dept": "Life Enrichment",
"context": (
"Alma was on the returned list but the Title / Role column was blank. "
"I see she is in Life Enrichment — is she an admin, manager, or something else?"
),
"questions": [
"What is Alma's title or role? (It will go on her account and email signature.)",
],
},
{
"name": "Polett Pinazavala — NOT a question, just a heads-up",
"dept": "Caregivers (Memory Care, MedTech, TueSat)",
"context": (
"Polett was on an earlier caregiver roster (MedTech, Memory Care, AM shift) but she was "
"not on the list you sent back. Howard has confirmed she is still an employee. Unless you "
"tell me otherwise, this is the setup she will get:"
),
"questions": [
"MedTech — Memory Care (TueSat), Desktop + phone, ALIS access, NO outside sign-in. "
"Stop me below if any of that is wrong — otherwise no action needed.",
],
},
{
"name": "Ederick Yuzon",
"dept": "Caregivers (Tower, TueSat)",
"context": (
"Just want to match the spelling on his payroll / ID so his account name is correct."
),
"questions": [
"Is his first name spelled \"Ederick\", \"Edrick\", or something else?",
],
},
{
"name": "Reliable Agency caregiver #1 (shared login)",
"dept": "Caregivers — Agency",
"context": (
"John added this agency row without a specific person's name, so I am treating it as a "
"shared login — whichever Reliable Agency caregiver is on shift signs in with this account. "
"That works, but I want to keep the username short."
),
"questions": [
"What short username would you like for this shared account? "
"`reliable.agency.caregiver1` is long — I can use `reliable1` instead. OK, or prefer something else?",
],
},
{
"name": "Reliable Agency caregiver #2 (shared login)",
"dept": "Caregivers — Agency",
"context": (
"Same situation as #1."
),
"questions": [
"Short username for the second shared agency login? Proposed: `reliable2`.",
],
},
]
CLOSING = (
"Once I have these answers back, I will set up every account in one pass and let you know "
"when they are ready for the users to sign in. Thank you!"
)
# -----------------------------------------------------------------------------
# OOXML building
# -----------------------------------------------------------------------------
NSW = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
def para(text: str, *, style: str | None = None, bold: bool = False, size: int | None = None) -> str:
"""A single <w:p> with one run. Font size is in half-points."""
pPr = ""
if style:
pPr = f'<w:pPr><w:pStyle w:val="{style}"/></w:pPr>'
rPr_parts = []
if bold:
rPr_parts.append("<w:b/>")
if size is not None:
rPr_parts.append(f'<w:sz w:val="{size}"/>')
rPr = f"<w:rPr>{''.join(rPr_parts)}</w:rPr>" if rPr_parts else ""
return (
f'<w:p>{pPr}'
f'<w:r>{rPr}<w:t xml:space="preserve">{escape(text)}</w:t></w:r>'
f'</w:p>'
)
def bullet(text: str) -> str:
return (
'<w:p><w:pPr><w:pStyle w:val="ListParagraph"/>'
'<w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr></w:pPr>'
f'<w:r><w:t xml:space="preserve">{escape(text)}</w:t></w:r></w:p>'
)
def blank_answer_line() -> str:
"""A hand-written answer placeholder: 'Answer: ________________'."""
return (
'<w:p>'
'<w:r><w:rPr><w:i/><w:color w:val="808080"/></w:rPr>'
'<w:t xml:space="preserve">Answer: </w:t></w:r>'
'<w:r><w:rPr><w:color w:val="C0C0C0"/></w:rPr>'
'<w:t xml:space="preserve">____________________________________________________________</w:t></w:r>'
'</w:p>'
)
def section_for(q: dict) -> str:
parts = []
parts.append(para(q["name"], style="Heading2"))
parts.append(para(q["dept"], bold=True, size=20))
parts.append(para(q["context"]))
for ques in q["questions"]:
parts.append(bullet(ques))
parts.append(blank_answer_line())
parts.append(para("")) # spacer
return "".join(parts)
def build_document_xml() -> str:
body_parts = []
body_parts.append(para(TITLE, style="Title"))
body_parts.append(para(SUBTITLE, bold=False, size=20))
body_parts.append(para(""))
body_parts.append(para(INTRO))
body_parts.append(para(""))
for q in QUESTIONS:
body_parts.append(section_for(q))
body_parts.append(para(""))
body_parts.append(para(CLOSING))
body = "".join(body_parts)
return (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
f'<w:document xmlns:w="{NSW}">'
f'<w:body>{body}'
'<w:sectPr>'
'<w:pgSz w:w="12240" w:h="15840"/>'
'<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>'
'</w:sectPr>'
'</w:body>'
'</w:document>'
)
STYLES_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:docDefaults>
<w:rPrDefault>
<w:rPr>
<w:rFonts w:ascii="Calibri" w:hAnsi="Calibri" w:cs="Calibri"/>
<w:sz w:val="22"/>
</w:rPr>
</w:rPrDefault>
<w:pPrDefault>
<w:pPr><w:spacing w:after="120"/></w:pPr>
</w:pPrDefault>
</w:docDefaults>
<w:style w:type="paragraph" w:styleId="Title">
<w:name w:val="Title"/>
<w:pPr><w:spacing w:before="240" w:after="120"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="44"/><w:color w:val="1A2030"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="Heading2">
<w:name w:val="heading 2"/>
<w:pPr><w:spacing w:before="360" w:after="80"/></w:pPr>
<w:rPr><w:b/><w:sz w:val="28"/><w:color w:val="2B6CB0"/></w:rPr>
</w:style>
<w:style w:type="paragraph" w:styleId="ListParagraph">
<w:name w:val="List Paragraph"/>
<w:pPr><w:ind w:left="720"/><w:contextualSpacing/></w:pPr>
</w:style>
</w:styles>
'''
NUMBERING_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:abstractNum w:abstractNumId="0">
<w:lvl w:ilvl="0">
<w:start w:val="1"/>
<w:numFmt w:val="bullet"/>
<w:lvlText w:val="&#8226;"/>
<w:lvlJc w:val="left"/>
<w:pPr><w:ind w:left="720" w:hanging="360"/></w:pPr>
<w:rPr><w:rFonts w:ascii="Symbol" w:hAnsi="Symbol"/></w:rPr>
</w:lvl>
</w:abstractNum>
<w:num w:numId="1"><w:abstractNumId w:val="0"/></w:num>
</w:numbering>
'''
CONTENT_TYPES_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
<Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>
<Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
</Types>
'''
ROOT_RELS_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>
'''
DOC_RELS_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
</Relationships>
'''
def build_docx(path: str) -> None:
os.makedirs(os.path.dirname(path), exist_ok=True)
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
z.writestr("[Content_Types].xml", CONTENT_TYPES_XML)
z.writestr("_rels/.rels", ROOT_RELS_XML)
z.writestr("word/_rels/document.xml.rels", DOC_RELS_XML)
z.writestr("word/document.xml", build_document_xml())
z.writestr("word/styles.xml", STYLES_XML)
z.writestr("word/numbering.xml", NUMBERING_XML)
if __name__ == "__main__":
build_docx(OUT)
print(f"Wrote {OUT}")