$entry) { if (!empty($entry['assoc_county'])) $counties[$entry['assoc_county']] = true; if (!empty($entry['assoc_type'])) $types[$entry['assoc_type']] = true; if (!empty($entry['assoc_sos_standing'])) $statuses[$entry['assoc_sos_standing']] = true; } ksort($counties); ksort($types); ksort($statuses); $total = count($registry); $out = '
'; // Header $out .= '
'; $out .= '

Associations

'; $out .= '
'; $out .= '+ Add Association'; $out .= 'Manage Fields'; $out .= 'Import'; $out .= '
'; if (empty($registry)) { $out .= '
No associations registered. Add the first one.
'; $out .= '
'; return $out; } // Search and filter bar $out .= ''; // Bulk bar (hidden until selection) $out .= ''; // Hidden export form $out .= ''; // Table $out .= '
'; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; foreach ($registry as $slug => $entry) { $type = $entry['assoc_type'] ?? ''; $county = $entry['assoc_county'] ?? ''; $status = $entry['assoc_sos_standing'] ?? ''; $status_class = 'civicinfra-status-unknown'; if ($status === 'Active') $status_class = 'civicinfra-status-active'; if ($status === 'Dissolved') $status_class = 'civicinfra-status-alert'; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; } $out .= '
SlugLegal NameTypeCountyUnitsStandingUpdated
' . assoc_h($slug) . '' . assoc_h($entry['assoc_legal_name'] ?? '—') . '' . assoc_h($type ?: '—') . '' . assoc_h($county ?: '—') . '' . assoc_h($entry['assoc_units'] ?? '—') . '' . assoc_h($status ?: 'UNK') . '' . assoc_h($entry['assoc_updated'] ?? '—') . 'Edit
'; // Pagination controls (JS-driven) $out .= '
'; $out .= '' . $total . ' associations'; $out .= '
'; $out .= ''; $out .= ''; $out .= '
'; $out .= ''; return $out; } // ---------------------------------------------------------------------------- // ADD ASSOCIATION // ---------------------------------------------------------------------------- function assoc_render_add_association_form() { $out = '
'; $out .= '
'; $out .= '

Add Association

'; $out .= '
'; $out .= '← Back'; $out .= '
'; $out .= '
'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= '
'; $out .= ''; $out .= '

Lowercase, hyphens only. Cannot be changed after creation. Used in URLs and as the key in config.json.

'; $out .= ''; $out .= '
'; $out .= ''; $out .= '
'; return $out; } // ---------------------------------------------------------------------------- // EDIT ASSOCIATION // ---------------------------------------------------------------------------- function assoc_render_edit_association_form($slug) { $registry = assoc_load_registry(); if (!isset($registry[$slug])) { return '
Association not found: ' . assoc_h($slug) . '
'; } $entry = $registry[$slug]; $def = assoc_load_fields(); $groups = $def['groups'] ?? []; $fields = $def['fields'] ?? []; $by_group = []; foreach ($fields as $f) { $by_group[$f['group']][] = $f; } $name = $entry['assoc_legal_name'] ?? $slug; $out = '
'; $out .= '
'; $out .= '
'; $out .= ''; $out .= '

' . assoc_h($name) . '

'; $out .= '

' . assoc_h($slug) . '

'; $out .= '
'; $out .= '
'; $out .= 'View profile'; $out .= '← Back'; $out .= '
'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= ''; foreach ($groups as $group) { $group_fields = $by_group[$group] ?? []; if (empty($group_fields)) continue; $out .= '
'; $out .= '

' . assoc_h($group) . '

'; foreach ($group_fields as $f) { $nick = $f['nickname']; $val = $entry[$nick] ?? ($f['default'] ?? ''); $type = $f['type'] ?? 'text'; $label = $f['label'] ?? $nick; $help = $f['help'] ?? ''; if ($type === 'readonly') { $out .= '
'; $out .= '

' . assoc_h($label) . '

'; $out .= '

' . assoc_h($val ?: '—') . '

'; $out .= '
'; continue; } $out .= '
'; $out .= ''; if ($help) $out .= '

' . assoc_h($help) . '

'; if ($type === 'select' && !empty($f['options'])) { $out .= ''; } else { $width = in_array($nick, ['assoc_legal_name','assoc_street','assoc_cai_listing', 'assoc_bbb_standing','assoc_idfpr_complaint', 'assoc_litigation_active','assoc_liens_active', 'assoc_mgmt_contact']) ? '100%' : '24rem'; $out .= ''; } $out .= '
'; } $out .= '
'; } $out .= '
'; $out .= ''; $out .= 'Cancel'; $out .= '
'; // Delete section $out .= '
'; $out .= '
Delete Association
'; $out .= '

This removes ' . assoc_h($name) . ' from the registry permanently. '; $out .= 'You must also remove the corresponding entry from vs01/config.json manually.

'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= ''; $out .= ''; $out .= '
'; $out .= '
'; return $out; } // ---------------------------------------------------------------------------- // FIELDS MANAGEMENT // ---------------------------------------------------------------------------- function assoc_render_fields_form() { $def = assoc_load_fields(); $groups = $def['groups'] ?? []; $fields = $def['fields'] ?? []; $out = '
'; $out .= '
'; $out .= '

Manage Fields

'; $out .= '
'; $out .= '← Back'; $out .= '
'; $out .= '
'; $out .= 'Adding a field backfills all existing associations with the default value. '; $out .= 'Removing a field deletes its data from all associations and cannot be undone.'; $out .= '
'; // Existing fields $out .= '
'; $out .= '

Existing Fields

'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= '
'; $out .= ''; $out .= ''; foreach ($fields as $f) { $nick = $f['nickname']; $type = $f['type'] ?? 'text'; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; $out .= ''; } $out .= '
Nickname Label Type Group Help text
' . assoc_h($nick) . '' . assoc_h($type) . ''; if ($type !== 'readonly') { $out .= ''; } $out .= '
'; $out .= '
'; $out .= '
'; // Hidden remove forms foreach ($fields as $f) { if (($f['type'] ?? '') === 'readonly') continue; $nick = $f['nickname']; $out .= ''; } // Add new field $out .= '
'; $out .= '

Add New Field

'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= '
'; $out .= '
'; $out .= '
'; $out .= '
'; $out .= '
'; $out .= ''; foreach ($groups as $g) { $out .= '
'; $out .= '
'; $out .= '
'; $out .= '
'; $out .= '
'; $out .= '
'; $out .= '
'; // Group order $out .= '
'; $out .= '

Group Order

'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= '
'; $out .= ''; $out .= ''; $out .= '
'; $out .= ''; $out .= '
'; $out .= '
'; return $out; } // ---------------------------------------------------------------------------- // EXPORT (download JSON) // ---------------------------------------------------------------------------- function assoc_handle_export($slugs_csv) { $registry = assoc_load_registry(); $slugs = array_filter(array_map('trim', explode(',', $slugs_csv))); $export = []; foreach ($slugs as $slug) { if (isset($registry[$slug])) { $export[$slug] = $registry[$slug]; } } if (empty($export)) { return '
No matching associations found for export.
'; } $json = json_encode($export, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); $filename = 'assoc-export-' . date('Y-m-d') . '.json'; header('Content-Type: application/json'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Length: ' . strlen($json)); echo $json; exit; } // ---------------------------------------------------------------------------- // IMPORT PAGE // ---------------------------------------------------------------------------- function assoc_render_import_form() { $out = '
'; $out .= '
'; $out .= '

Import Associations

'; $out .= '
'; $out .= '← Back'; $out .= '
'; $out .= '
'; $out .= 'Upload a JSON export file. New associations will be added. Existing associations with changed data will be shown for your review before any changes are applied.'; $out .= '
'; $out .= '
'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= '
'; $out .= ''; $out .= ''; $out .= '
'; $out .= ''; $out .= '
'; return $out; } // ---------------------------------------------------------------------------- // IMPORT — HANDLE UPLOAD AND PRODUCE DIFF // ---------------------------------------------------------------------------- function assoc_handle_import_upload() { if (empty($_FILES['import_file']['tmp_name'])) { return '
No file received.
'; } $raw = @file_get_contents($_FILES['import_file']['tmp_name']); if (!$raw) { return '
Could not read uploaded file.
'; } $incoming = json_decode($raw, true); if (json_last_error() !== JSON_ERROR_NONE) { return '
Invalid JSON in uploaded file.
'; } // Strip meta keys foreach (['_note','_version','_slug_format','_select_values'] as $k) unset($incoming[$k]); $registry = assoc_load_registry(); $new = []; $changed = []; $same = []; foreach ($incoming as $slug => $entry) { if (!isset($registry[$slug])) { $new[$slug] = $entry; } else { $diffs = assoc_diff_entry($registry[$slug], $entry); if (!empty($diffs)) { $changed[$slug] = ['current' => $registry[$slug], 'incoming' => $entry, 'diffs' => $diffs]; } else { $same[$slug] = true; } } } if (empty($new) && empty($changed)) { return '
No changes found. The uploaded file matches the current registry.
' . '← Back'; } // Encode the incoming data for the confirmation form $encoded = base64_encode(json_encode($incoming)); $out = '
'; $out .= '
'; $out .= '

Review Import

'; $out .= '
'; if (!empty($new)) { $out .= '
'; $out .= '

New — ' . count($new) . ' association' . (count($new) > 1 ? 's' : '') . '

'; $out .= '

These do not exist in the current registry and will be added.

'; $out .= '
'; } if (!empty($changed)) { $out .= '
'; $out .= '

Changed — ' . count($changed) . ' association' . (count($changed) > 1 ? 's' : '') . '

'; $out .= '

These exist in the registry with different values. Choose to import or skip each one.

'; $out .= '
'; foreach ($changed as $slug => $data) { $name = $data['current']['assoc_legal_name'] ?? $slug; $out .= '
'; $out .= '
'; $out .= '' . assoc_h($name) . ''; $out .= '' . assoc_h($slug) . ''; $out .= '
'; $out .= ''; $out .= ''; $out .= '
'; $out .= '
'; foreach ($data['diffs'] as $field => $diff) { $out .= '' . assoc_h($field) . ': ' . assoc_h($diff['current']) . ''; $out .= '' . assoc_h($field) . ': ' . assoc_h($diff['incoming']) . ''; } $out .= '
'; } $out .= '
'; } if (!empty($same)) { $out .= '

' . count($same) . ' association' . (count($same) > 1 ? 's' : '') . ' unchanged — skipped.

'; } $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= ''; // Collect diff decisions foreach ($changed as $slug => $data) { $out .= ''; } $out .= '
'; $out .= ''; $out .= 'Upload different file'; $out .= 'Cancel'; $out .= '
'; return $out; } // ---------------------------------------------------------------------------- // IMPORT — CONFIRM AND APPLY // ---------------------------------------------------------------------------- function assoc_handle_import_confirm() { $encoded = $_POST['import_data'] ?? ''; $incoming = json_decode(base64_decode($encoded), true); if (!$incoming) { return '
Import data missing or corrupted.
'; } $decisions = $_POST['diff_decisions'] ?? []; $registry = assoc_load_registry(); $added = 0; $updated = 0; $skipped = 0; foreach ($incoming as $slug => $entry) { if (!isset($registry[$slug])) { // New — always add $entry['assoc_registered'] = $entry['assoc_registered'] ?? date('Y-m-d'); $entry['assoc_updated'] = date('Y-m-d'); $registry[$slug] = $entry; $added++; } else { // Existing — check decision $decision = $decisions[$slug] ?? 'import'; if ($decision === 'skip') { $skipped++; } else { $entry['assoc_updated'] = date('Y-m-d'); $registry[$slug] = $entry; $updated++; } } } if (!assoc_write_registry($registry)) { return '
Failed to write registry. Check server logs.
'; } $out = '
'; $out .= 'Import complete. '; if ($added) $out .= $added . ' added. '; if ($updated) $out .= $updated . ' updated. '; if ($skipped) $out .= $skipped . ' skipped.'; $out .= '
'; $out .= '← Back to Associations'; return $out; } // ---------------------------------------------------------------------------- // DIFF HELPER // ---------------------------------------------------------------------------- function assoc_diff_entry($current, $incoming) { $diffs = []; $keys = array_unique(array_merge(array_keys($current), array_keys($incoming))); foreach ($keys as $key) { $cv = $current[$key] ?? ''; $iv = $incoming[$key] ?? ''; if ($cv !== $iv) { $diffs[$key] = ['current' => $cv, 'incoming' => $iv]; } } return $diffs; }