$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 .= '';
if (empty($registry)) {
$out .= '
';
$out .= '
';
return $out;
}
// Search and filter bar — single row
$out .= '';
$out .= '
';
$out .= ' ';
$out .= '
';
$out .= '
';
$out .= 'All counties ';
foreach ($counties as $c => $_) { $out .= '' . assoc_h($c) . ' '; }
$out .= ' ';
$out .= '
';
$out .= 'All types ';
foreach ($types as $t => $_) { $out .= '' . assoc_h($t === 'UNK' ? 'Unknown' : $t) . ' '; }
$out .= ' ';
$out .= '
';
$out .= 'All standing ';
foreach ($statuses as $s => $_) { $out .= '' . assoc_h($s === 'UNK' ? 'Unknown' : $s) . ' '; }
$out .= ' ';
$out .= '
';
// Bulk bar (hidden until selection)
$out .= '';
$out .= '0 selected ';
$out .= 'Export selected ';
$out .= '
';
// Hidden export form
$out .= '';
// Table
$out .= '';
// Pagination controls (JS-driven)
$out .= '';
$out .= '';
return $out;
}
// ----------------------------------------------------------------------------
// ADD ASSOCIATION
// ----------------------------------------------------------------------------
function assoc_render_add_association_form() {
$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 .= '
';
// 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 .= '
';
return $out;
}
// ----------------------------------------------------------------------------
// FIELDS MANAGEMENT
// ----------------------------------------------------------------------------
function assoc_render_fields_form() {
$def = assoc_load_fields();
$groups = $def['groups'] ?? [];
$fields = $def['fields'] ?? [];
$out = '';
$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 .= '
';
// Hidden remove forms
foreach ($fields as $f) {
if (($f['type'] ?? '') === 'readonly') continue;
$nick = $f['nickname'];
$out .= '
';
}
// Add new field
$out .= '
';
// Group order
$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 .= '
';
$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 .= '
';
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 .= '';
if (!empty($new)) {
$out .= '
';
}
if (!empty($changed)) {
$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 .= ' ';
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;
}