| Server IP : 10.200.247.200 / Your IP : 216.73.217.19 Web Server : Apache System : Linux synergy-usa-sites 6.8.0-138-generic #138-Ubuntu SMP PREEMPT_DYNAMIC Fri Jul 31 22:41:49 UTC 2026 x86_64 User : jeremy ( 1001) PHP Version : 8.4.25 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/development/callings.lvsaints.com/ |
Upload File : |
# Village Calling Board — Implementation Plan (Hybrid)
Goal: a **functional, data-driven** calling board (working buttons, org switching,
LCR import, per-ward data) **plus** safe design levers so the boss keeps the
styling control he wants — without letting hand-edited HTML break the app.
Stack: **PHP 8.4 + MariaDB** (server-rendered) with a light JavaScript layer
(vanilla / Alpine + SortableJS). No build step.
---
## Guiding principle: separate the three layers
| Layer | Who owns it | How it's changed |
|---|---|---|
| **Data** (members, callings, planning) | LCR + bishopric | LCR import + working buttons |
| **Structure** (templates, logic) | Developer | Code |
| **Theme** (CSS, colors, spacing, ordering) | Boss | "Design" tab (editable CSS per ward) |
The old "upload a full HTML file" feature is **kept but rebranded as
"Design Proposals"** — a sandbox for mocking up new directions, which then get
folded into the real app. It is no longer the live site.
---
## Data model (new tables, added alongside wards / users / page_versions)
- **organizations** — left-menu items (Bishopric, EQ, RS, YM, YW, Primary, …),
with `sort_order`. Global reference list.
- **calling_areas** — the *cards* on the board (e.g. "Primary Presidency",
"Valiant 9", "Nursery", "Activity Days – Jr Boys"). Fields: `ward_id`,
`organization_id`, `section` (group header), `name`, `sort_order`.
- **members** — the ward roster from LCR. `ward_id`, `name`, `category`
(member / student / mission), `notes`.
- **current_callings** — LCR truth. `ward_id`, `calling_area_id`,
`calling_name` (role), `member_id` (NULL = vacant), `sustained_date`,
`imported_at`.
- **planning_items** — board truth (per README). `ward_id`, `member_id`,
`organization_id`, `calling_area_id`, `calling_name`,
`status` (idea | proposed | extend | hold), `notes`,
`created_by_user_id`, `created_at`, `updated_at`, `archived_at`.
- **import_history** — `ward_id`, `type` (callings | members), `raw_text`,
`summary`, `created_by`, `created_at`.
- **ward_settings** — `ward_id` (PK), `theme_css` (LONGTEXT), `layout_json`.
`page_versions` stays as the Design-Proposals store.
Derived, not stored: **Needs Calling** = members with no current calling
MINUS anyone already in Ideas / Proposed / Extend (per README).
---
## Backend architecture
- `index.php` → **app shell**: reads `?org=primary&ward=…`, renders the board
and planning sidebar from the DB, and injects the ward's `theme_css`.
- `render/board.php`, `render/sidebar.php` → shared templates (the README's
"reuse the same components for every org").
- `api/` → small JSON endpoints, each enforcing login + ward scope + CSRF:
- `api/planning.php` — add / remove / move / update planning items.
- `api/import.php` — parse pasted LCR reports.
- `api/theme.php` — save theme CSS / layout.
- `assets/app.js` → button clicks & drag/drop call the API and update the DOM.
Roles already built: **super_admin** (all wards), **admin** (own ward),
**viewer** (read-only). All API calls reuse `can_edit_ward()` / `can_view_ward()`.
---
## Phases (each is independently demo-able)
### Phase 1 — Data model + read-only board + org switching
- Create the new tables; seed `organizations`.
- One-time seed: convert the current static Primary content into rows so the
board looks identical to today, but rendered from the DB.
- Refactor `index.php` into the app shell; left menu switches org via `?org=`.
- **Outcome:** clicking "Young Men" loads Young Men; nothing is hard-coded.
### Phase 2 — Working planning buttons (CRUD)
- `+ Idea`, remove idea, Hold, Extend, "Archive / Clear Current Ideas".
- Planning sidebar generated live (Needs Calling / Ideas-Proposed / Extend /
Hold / Mission / YSA).
- **Outcome:** the buttons do real things and persist.
### Phase 3 — LCR import
- Parse "Callings by Organization" and "Members Without Callings".
- Update `current_callings` + `members`; **preserve** planning items.
- Auto-resolve: someone in Proposed/Extend who shows up sustained in LCR is
removed from planning and shown as current.
- Record each import in `import_history`.
- **Outcome:** the paste-from-LCR workflow works end to end.
### Phase 4 — Drag & drop
- SortableJS: drag a member from "Needs Calling" onto a card → creates a
planning item; removing it returns them to Needs Calling (if still uncalled).
- **Outcome:** the interaction described in the README.
### Phase 5 — Design tab + Proposals rebrand
- Admin "Design" tab: edit `theme_css` per ward with a live preview iframe;
saved CSS is injected into the board so the boss can restyle/reorder safely.
- Rebrand the current upload feature as "Design Proposals" (mockups, not live).
- **Outcome:** boss gets more design control than today, logic stays intact.
### Phase 6 — Polish (later)
- Member detail panel + notes, sustained dates on hover, per-ward URL slugs
(`/Village`, `/Southgate`), scenarios.
---
## Security notes
- Every API endpoint: `require_login()` + ward-scope check + CSRF + prepared
statements.
- `theme_css` is injected into a `<style>` tag. Only admins can set it (same
trust level as today's HTML upload), but strip `</style>`/`<` breakouts on
save so a stray tag can't inject script. Consider a CSP header too.
- Member data stays behind login (already enforced).
---
## Open design questions to settle during Phase 1
1. **Card mapping:** LCR gives a flat calling list; the board groups them into
cards ("Valiant 9", "Nursery"). Do we define `calling_areas` by hand per
ward and map LCR calling names into them, or auto-derive from LCR text?
(Recommend: hand-defined areas + a mapping table, refined over time.)
2. **JS approach:** vanilla + fetch vs. Alpine.js. (Recommend Alpine for the
drag/reactive bits; still no build step.)