Updated
This commit is contained in:
688
hubzilla/addon/assoc_profile/view/css/assoc_profile_manage.php
Normal file
688
hubzilla/addon/assoc_profile/view/css/assoc_profile_manage.php
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Association Profile — Management Renderer
|
||||||
|
* Operator-only. Called from assoc_profile.php content router.
|
||||||
|
* v0.2.0 — search, filter, paginate, bulk export, import with diff, delete.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// MANAGE INDEX
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function assoc_render_manage_index() {
|
||||||
|
$registry = assoc_load_registry();
|
||||||
|
$def = assoc_load_fields();
|
||||||
|
$fields = $def['fields'] ?? [];
|
||||||
|
|
||||||
|
// Build filter option sets from registry data
|
||||||
|
$counties = [];
|
||||||
|
$types = [];
|
||||||
|
$statuses = [];
|
||||||
|
foreach ($registry as $slug => $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 = '<div class="assoc-manage">';
|
||||||
|
|
||||||
|
// Header
|
||||||
|
$out .= '<header class="civicinfra-manage-header">';
|
||||||
|
$out .= '<h1 class="civicinfra-manage-title">Associations</h1>';
|
||||||
|
$out .= '<div class="civicinfra-manage-actions">';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage/assoc" class="btn btn-primary btn-sm">+ Add Association</a>';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage/fields" class="btn btn-outline-secondary btn-sm">Manage Fields</a>';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage/import" class="btn btn-outline-secondary btn-sm">Import</a>';
|
||||||
|
$out .= '</div></header>';
|
||||||
|
|
||||||
|
if (empty($registry)) {
|
||||||
|
$out .= '<div class="civicinfra-notice">No associations registered. <a href="' . z_root() . '/assoc_profile/manage/assoc">Add the first one.</a></div>';
|
||||||
|
$out .= '</div>';
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search and filter bar — single horizontal row, no wrap
|
||||||
|
$out .= '<div class="assoc-search-bar" role="search">';
|
||||||
|
$out .= '<input type="search" id="assoc-search" class="form-control form-control-sm"
|
||||||
|
placeholder="Search name or slug…" aria-label="Search associations">';
|
||||||
|
|
||||||
|
$out .= '<select id="assoc-filter-county" class="form-select form-select-sm" aria-label="Filter by county">';
|
||||||
|
$out .= '<option value="">All counties</option>';
|
||||||
|
foreach ($counties as $c => $_) { $out .= '<option value="' . assoc_h($c) . '">' . assoc_h($c) . '</option>'; }
|
||||||
|
$out .= '</select>';
|
||||||
|
|
||||||
|
$out .= '<select id="assoc-filter-type" class="form-select form-select-sm" aria-label="Filter by type">';
|
||||||
|
$out .= '<option value="">All types</option>';
|
||||||
|
foreach ($types as $t => $_) { $out .= '<option value="' . assoc_h($t) . '">' . assoc_h($t === 'UNK' ? 'Unknown' : $t) . '</option>'; }
|
||||||
|
$out .= '</select>';
|
||||||
|
|
||||||
|
$out .= '<select id="assoc-filter-status" class="form-select form-select-sm" aria-label="Filter by standing">';
|
||||||
|
$out .= '<option value="">All standing</option>';
|
||||||
|
foreach ($statuses as $s => $_) { $out .= '<option value="' . assoc_h($s) . '">' . assoc_h($s === 'UNK' ? 'Unknown' : $s) . '</option>'; }
|
||||||
|
$out .= '</select>';
|
||||||
|
|
||||||
|
$out .= '</div>';
|
||||||
|
|
||||||
|
// Bulk bar (hidden until selection)
|
||||||
|
$out .= '<div id="assoc-bulk-bar" class="assoc-bulk-bar" style="display:none;">';
|
||||||
|
$out .= '<span id="assoc-bulk-count" class="me-2">0 selected</span>';
|
||||||
|
$out .= '<button type="button" id="assoc-export-selected" class="btn btn-sm btn-outline-secondary">Export selected</button>';
|
||||||
|
$out .= '</div>';
|
||||||
|
|
||||||
|
// Hidden export form
|
||||||
|
$out .= '<form id="assoc-export-form" method="post" action="' . z_root() . '/assoc_profile/manage" style="display:none;">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="export_selected">';
|
||||||
|
$out .= '<input type="hidden" id="assoc-export-slugs" name="export_slugs" value="">';
|
||||||
|
$out .= '</form>';
|
||||||
|
|
||||||
|
// Table
|
||||||
|
$out .= '<div class="civicinfra-data-table">';
|
||||||
|
$out .= '<table class="table table-sm mb-0" id="assoc-manage-table">';
|
||||||
|
$out .= '<thead><tr>';
|
||||||
|
$out .= '<th scope="col" style="width:2rem;"><input type="checkbox" id="assoc-select-all" class="form-check-input" title="Select all visible"></th>';
|
||||||
|
$out .= '<th scope="col">Slug</th>';
|
||||||
|
$out .= '<th scope="col">Legal Name</th>';
|
||||||
|
$out .= '<th scope="col">Units</th>';
|
||||||
|
$out .= '<th scope="col">Updated</th>';
|
||||||
|
$out .= '<th scope="col"></th>';
|
||||||
|
$out .= '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
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 .= '<tr data-slug="' . assoc_h($slug) . '"'
|
||||||
|
. ' data-name="' . assoc_h($entry['assoc_legal_name'] ?? '') . '"'
|
||||||
|
. ' data-county="' . assoc_h($county) . '"'
|
||||||
|
. ' data-type="' . assoc_h($type) . '"'
|
||||||
|
. ' data-status="' . assoc_h($status) . '">';
|
||||||
|
|
||||||
|
$out .= '<td><input type="checkbox" name="assoc_select[]" value="' . assoc_h($slug) . '" class="form-check-input"></td>';
|
||||||
|
|
||||||
|
$out .= '<td><a href="' . z_root() . '/channel/' . assoc_h($slug) . '"
|
||||||
|
class="text-decoration-none" title="View channel">'
|
||||||
|
. '<code class="civicinfra-record-id">' . assoc_h($slug) . '</code>'
|
||||||
|
. '</a></td>';
|
||||||
|
|
||||||
|
$out .= '<td>' . assoc_h($entry['assoc_legal_name'] ?? '—') . '</td>';
|
||||||
|
$out .= '<td class="civicinfra-record-meta">' . assoc_h($entry['assoc_units'] ?? '—') . '</td>';
|
||||||
|
$out .= '<td class="civicinfra-record-meta">' . assoc_h($entry['assoc_updated'] ?? '—') . '</td>';
|
||||||
|
$out .= '<td><a href="' . z_root() . '/assoc_profile/manage/assoc/' . assoc_h($slug) . '"
|
||||||
|
class="btn btn-sm btn-outline-secondary">Edit</a></td>';
|
||||||
|
$out .= '</tr>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$out .= '</tbody></table></div>';
|
||||||
|
|
||||||
|
// Pagination controls (JS-driven)
|
||||||
|
$out .= '<div class="assoc-pagination">';
|
||||||
|
$out .= '<span id="assoc-page-info" class="civicinfra-record-meta">' . $total . ' associations</span>';
|
||||||
|
$out .= '<div class="d-flex gap-2">';
|
||||||
|
$out .= '<button id="assoc-page-prev" class="btn btn-sm btn-outline-secondary" disabled>← Prev</button>';
|
||||||
|
$out .= '<button id="assoc-page-next" class="btn btn-sm btn-outline-secondary">Next →</button>';
|
||||||
|
$out .= '</div></div>';
|
||||||
|
|
||||||
|
$out .= '</div>';
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// ADD ASSOCIATION
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function assoc_render_add_association_form() {
|
||||||
|
$out = '<div class="assoc-manage">';
|
||||||
|
$out .= '<header class="civicinfra-manage-header">';
|
||||||
|
$out .= '<h1 class="civicinfra-manage-title">Add Association</h1>';
|
||||||
|
$out .= '<div class="civicinfra-manage-actions">';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary btn-sm">← Back</a>';
|
||||||
|
$out .= '</div></header>';
|
||||||
|
|
||||||
|
$out .= '<div class="civicinfra-form-section">';
|
||||||
|
$out .= '<form method="post" action="' . z_root() . '/assoc_profile/manage">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="add_association">';
|
||||||
|
$out .= '<div class="mb-3">';
|
||||||
|
$out .= '<label class="form-label" for="new_slug">Slug</label>';
|
||||||
|
$out .= '<p class="civicinfra-help-text mb-1">Lowercase, hyphens only. Cannot be changed after creation. Used in URLs and as the key in config.json.</p>';
|
||||||
|
$out .= '<input type="text" id="new_slug" class="form-control" name="new_slug"
|
||||||
|
placeholder="e.g. arbors-bgca" required pattern="[a-z0-9\-]+"
|
||||||
|
style="max-width:24rem;">';
|
||||||
|
$out .= '</div>';
|
||||||
|
$out .= '<button type="submit" class="btn btn-primary">Create Association</button>';
|
||||||
|
$out .= '</form></div></div>';
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// EDIT ASSOCIATION
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function assoc_render_edit_association_form($slug) {
|
||||||
|
$registry = assoc_load_registry();
|
||||||
|
if (!isset($registry[$slug])) {
|
||||||
|
return '<div class="civicinfra-notice civicinfra-warning">Association not found: <code>' . assoc_h($slug) . '</code></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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;
|
||||||
|
|
||||||
|
// Tab label overrides — short labels for tabs
|
||||||
|
$tab_labels = [
|
||||||
|
'Identity' => 'Identity',
|
||||||
|
'Physical Structure' => 'Buildings',
|
||||||
|
'Governance and Management'=> 'Governance',
|
||||||
|
'Legal and Compliance' => 'Compliance',
|
||||||
|
'Record' => 'Record',
|
||||||
|
];
|
||||||
|
|
||||||
|
$out = '<div class="assoc-manage">';
|
||||||
|
$out .= '<header class="civicinfra-manage-header">';
|
||||||
|
$out .= '<div>';
|
||||||
|
$out .= '<nav aria-label="breadcrumb"><ol class="breadcrumb mb-1">';
|
||||||
|
$out .= '<li class="breadcrumb-item"><a href="' . z_root() . '/assoc_profile/manage">Associations</a></li>';
|
||||||
|
$out .= '<li class="breadcrumb-item active">' . assoc_h($name) . '</li>';
|
||||||
|
$out .= '</ol></nav>';
|
||||||
|
$out .= '<h1 class="civicinfra-manage-title">' . assoc_h($name) . '</h1>';
|
||||||
|
$out .= '<p class="civicinfra-record-meta mt-1"><code class="civicinfra-record-id">' . assoc_h($slug) . '</code></p>';
|
||||||
|
$out .= '</div>';
|
||||||
|
$out .= '<div class="civicinfra-manage-actions">';
|
||||||
|
$out .= '<a href="' . z_root() . '/profile/' . assoc_h($slug) . '" class="btn btn-outline-secondary btn-sm" target="_blank">View profile</a>';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary btn-sm">← Back</a>';
|
||||||
|
$out .= '</div></header>';
|
||||||
|
|
||||||
|
$out .= '<form method="post" action="' . z_root() . '/assoc_profile/manage">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="save_association">';
|
||||||
|
$out .= '<input type="hidden" name="assoc_slug" value="' . assoc_h($slug) . '">';
|
||||||
|
|
||||||
|
// Tab nav
|
||||||
|
$out .= '<ul class="nav nav-tabs assoc-edit-tabs mb-0" id="assocEditTabs" role="tablist">';
|
||||||
|
$first = true;
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
$group_fields = $by_group[$group] ?? [];
|
||||||
|
if (empty($group_fields)) continue;
|
||||||
|
$tab_id = 'tab-' . preg_replace('/[^a-z0-9]/', '-', strtolower($group));
|
||||||
|
$label = $tab_labels[$group] ?? $group;
|
||||||
|
$active = $first ? 'active' : '';
|
||||||
|
$selected = $first ? 'true' : 'false';
|
||||||
|
$out .= '<li class="nav-item" role="presentation">';
|
||||||
|
$out .= '<button class="nav-link ' . $active . '" id="' . assoc_h($tab_id) . '-btn"
|
||||||
|
data-bs-toggle="tab"
|
||||||
|
data-bs-target="#' . assoc_h($tab_id) . '-pane"
|
||||||
|
type="button" role="tab"
|
||||||
|
aria-selected="' . $selected . '">'
|
||||||
|
. assoc_h($label) . '</button>';
|
||||||
|
$out .= '</li>';
|
||||||
|
$first = false;
|
||||||
|
}
|
||||||
|
$out .= '</ul>';
|
||||||
|
|
||||||
|
// Tab panes
|
||||||
|
$out .= '<div class="tab-content assoc-edit-tab-content" id="assocEditTabContent">';
|
||||||
|
$first = true;
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
$group_fields = $by_group[$group] ?? [];
|
||||||
|
if (empty($group_fields)) continue;
|
||||||
|
$tab_id = 'tab-' . preg_replace('/[^a-z0-9]/', '-', strtolower($group));
|
||||||
|
$active = $first ? 'show active' : '';
|
||||||
|
|
||||||
|
$out .= '<div class="tab-pane fade ' . $active . ' civicinfra-form-section assoc-tab-pane"'
|
||||||
|
. ' id="' . assoc_h($tab_id) . '-pane" role="tabpanel">';
|
||||||
|
|
||||||
|
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 .= '<div class="mb-3">';
|
||||||
|
$out .= '<p class="civicinfra-help-text mb-0">' . assoc_h($label) . '</p>';
|
||||||
|
$out .= '<p class="mb-0">' . assoc_h($val ?: '—') . '</p>';
|
||||||
|
$out .= '</div>';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$out .= '<div class="mb-3">';
|
||||||
|
$out .= '<label class="form-label" for="' . assoc_h($nick) . '">' . assoc_h($label) . '</label>';
|
||||||
|
if ($help) $out .= '<p class="civicinfra-help-text mb-1">' . assoc_h($help) . '</p>';
|
||||||
|
|
||||||
|
if ($type === 'select' && !empty($f['options'])) {
|
||||||
|
$out .= '<select class="form-select form-select-sm" id="' . assoc_h($nick) . '"
|
||||||
|
name="' . assoc_h($nick) . '" style="max-width:20rem;">';
|
||||||
|
foreach ($f['options'] as $opt) {
|
||||||
|
$sel = ($val === $opt) ? 'selected' : '';
|
||||||
|
$disp = ($opt === 'UNK') ? 'Unknown — not yet verified' : $opt;
|
||||||
|
$out .= '<option value="' . assoc_h($opt) . '" ' . $sel . '>' . assoc_h($disp) . '</option>';
|
||||||
|
}
|
||||||
|
$out .= '</select>';
|
||||||
|
} else {
|
||||||
|
$wide = 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']);
|
||||||
|
$out .= '<input type="text" class="form-control form-control-sm"
|
||||||
|
id="' . assoc_h($nick) . '" name="' . assoc_h($nick) . '"
|
||||||
|
value="' . assoc_h($val) . '"'
|
||||||
|
. ($wide ? '' : ' style="max-width:24rem;"') . '>';
|
||||||
|
}
|
||||||
|
$out .= '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$out .= '</div>'; // tab-pane
|
||||||
|
$first = false;
|
||||||
|
}
|
||||||
|
$out .= '</div>'; // tab-content
|
||||||
|
|
||||||
|
// Save bar below tabs
|
||||||
|
$out .= '<div class="assoc-save-bar">';
|
||||||
|
$out .= '<button type="submit" class="btn btn-primary">Save</button> ';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary">Cancel</a>';
|
||||||
|
$out .= '</div>';
|
||||||
|
$out .= '</form>';
|
||||||
|
|
||||||
|
// Delete section
|
||||||
|
$out .= '<div class="assoc-delete-confirm">';
|
||||||
|
$out .= '<h5>Delete Association</h5>';
|
||||||
|
$out .= '<p class="civicinfra-help-text">Permanently removes <strong>' . assoc_h($name) . '</strong> from the registry. ';
|
||||||
|
$out .= 'You must also remove the entry from <code>vs01/config.json</code> manually.</p>';
|
||||||
|
$out .= '<form method="post" action="' . z_root() . '/assoc_profile/manage"
|
||||||
|
onsubmit="return confirm('Permanently delete ' . addslashes(assoc_h($name)) . '? This cannot be undone.')">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="delete_association">';
|
||||||
|
$out .= '<input type="hidden" name="assoc_slug" value="' . assoc_h($slug) . '">';
|
||||||
|
$out .= '<button type="submit" class="btn btn-danger btn-sm">Delete ' . assoc_h($name) . '</button>';
|
||||||
|
$out .= '</form></div>';
|
||||||
|
|
||||||
|
$out .= '</div>';
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// FIELDS MANAGEMENT
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function assoc_render_fields_form() {
|
||||||
|
$def = assoc_load_fields();
|
||||||
|
$groups = $def['groups'] ?? [];
|
||||||
|
$fields = $def['fields'] ?? [];
|
||||||
|
|
||||||
|
$out = '<div class="assoc-manage">';
|
||||||
|
$out .= '<header class="civicinfra-manage-header">';
|
||||||
|
$out .= '<h1 class="civicinfra-manage-title">Manage Fields</h1>';
|
||||||
|
$out .= '<div class="civicinfra-manage-actions">';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary btn-sm">← Back</a>';
|
||||||
|
$out .= '</div></header>';
|
||||||
|
|
||||||
|
$out .= '<div class="civicinfra-notice mb-4">';
|
||||||
|
$out .= 'Adding a field backfills all existing associations with the default value. ';
|
||||||
|
$out .= 'Removing a field deletes its data from <strong>all</strong> associations and cannot be undone.';
|
||||||
|
$out .= '</div>';
|
||||||
|
|
||||||
|
// Existing fields
|
||||||
|
$out .= '<section class="civicinfra-form-section">';
|
||||||
|
$out .= '<h2 class="civicinfra-record-heading">Existing Fields</h2>';
|
||||||
|
$out .= '<form method="post" action="' . z_root() . '/assoc_profile/manage">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="save_fields">';
|
||||||
|
$out .= '<div style="overflow-x:auto;">';
|
||||||
|
$out .= '<table class="table table-sm civicinfra-data-table">';
|
||||||
|
$out .= '<thead><tr>
|
||||||
|
<th>Nickname</th>
|
||||||
|
<th>Label</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Group</th>
|
||||||
|
<th>Help text</th>
|
||||||
|
<th></th>
|
||||||
|
</tr></thead><tbody>';
|
||||||
|
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
$nick = $f['nickname'];
|
||||||
|
$type = $f['type'] ?? 'text';
|
||||||
|
$out .= '<tr>';
|
||||||
|
$out .= '<td><code class="civicinfra-record-id">' . assoc_h($nick) . '</code></td>';
|
||||||
|
$out .= '<td><input type="text" class="form-control form-control-sm"
|
||||||
|
name="label_' . assoc_h($nick) . '"
|
||||||
|
value="' . assoc_h($f['label'] ?? '') . '"
|
||||||
|
style="min-width:10rem;"></td>';
|
||||||
|
$out .= '<td><span class="civicinfra-affordance">' . assoc_h($type) . '</span></td>';
|
||||||
|
$out .= '<td><input type="text" class="form-control form-control-sm"
|
||||||
|
name="group_' . assoc_h($nick) . '"
|
||||||
|
value="' . assoc_h($f['group'] ?? '') . '"
|
||||||
|
style="min-width:8rem;"></td>';
|
||||||
|
$out .= '<td><input type="text" class="form-control form-control-sm"
|
||||||
|
name="help_' . assoc_h($nick) . '"
|
||||||
|
value="' . assoc_h($f['help'] ?? '') . '"
|
||||||
|
style="min-width:14rem;"></td>';
|
||||||
|
$out .= '<td>';
|
||||||
|
if ($type !== 'readonly') {
|
||||||
|
$out .= '<button type="submit" form="rm-' . assoc_h($nick) . '" class="btn btn-danger btn-sm"
|
||||||
|
onclick="return confirm(\'Remove ' . assoc_h($nick) . ' from ALL associations? Cannot be undone.\')">Remove</button>';
|
||||||
|
}
|
||||||
|
$out .= '</td></tr>';
|
||||||
|
}
|
||||||
|
$out .= '</tbody></table></div>';
|
||||||
|
$out .= '<div class="mt-3"><button type="submit" class="btn btn-primary btn-sm">Save Changes</button></div>';
|
||||||
|
$out .= '</form></section>';
|
||||||
|
|
||||||
|
// Hidden remove forms
|
||||||
|
foreach ($fields as $f) {
|
||||||
|
if (($f['type'] ?? '') === 'readonly') continue;
|
||||||
|
$nick = $f['nickname'];
|
||||||
|
$out .= '<form id="rm-' . assoc_h($nick) . '" method="post" action="' . z_root() . '/assoc_profile/manage" style="display:none;">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="remove_field">';
|
||||||
|
$out .= '<input type="hidden" name="remove_nickname" value="' . assoc_h($nick) . '">';
|
||||||
|
$out .= '</form>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new field
|
||||||
|
$out .= '<section class="civicinfra-form-section">';
|
||||||
|
$out .= '<h2 class="civicinfra-record-heading">Add New Field</h2>';
|
||||||
|
$out .= '<form method="post" action="' . z_root() . '/assoc_profile/manage">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="add_field">';
|
||||||
|
$out .= '<div class="row g-3">';
|
||||||
|
$out .= '<div class="col-md-2"><label class="form-label">Nickname</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" name="new_nickname" placeholder="assoc_fieldname"></div>';
|
||||||
|
$out .= '<div class="col-md-2"><label class="form-label">Label</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" name="new_label"></div>';
|
||||||
|
$out .= '<div class="col-md-1"><label class="form-label">Type</label>
|
||||||
|
<select class="form-select form-select-sm" name="new_type">
|
||||||
|
<option value="text">text</option>
|
||||||
|
<option value="select">select</option>
|
||||||
|
</select></div>';
|
||||||
|
$out .= '<div class="col-md-2"><label class="form-label">Group</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" name="new_group" list="assoc-group-list">';
|
||||||
|
$out .= '<datalist id="assoc-group-list">';
|
||||||
|
foreach ($groups as $g) { $out .= '<option value="' . assoc_h($g) . '">'; }
|
||||||
|
$out .= '</datalist></div>';
|
||||||
|
$out .= '<div class="col-md-1"><label class="form-label">Default</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" name="new_default" placeholder="UNK"></div>';
|
||||||
|
$out .= '<div class="col-md-2"><label class="form-label">Options <span class="civicinfra-help-text">(select only, one per line)</span></label>
|
||||||
|
<textarea class="form-control form-control-sm" name="new_options" rows="3"></textarea></div>';
|
||||||
|
$out .= '<div class="col-md-2"><label class="form-label">Help text</label>
|
||||||
|
<input type="text" class="form-control form-control-sm" name="new_help"></div>';
|
||||||
|
$out .= '</div>';
|
||||||
|
$out .= '<div class="mt-3"><button type="submit" class="btn btn-primary btn-sm">Add Field</button></div>';
|
||||||
|
$out .= '</form></section>';
|
||||||
|
|
||||||
|
// Group order
|
||||||
|
$out .= '<section class="civicinfra-form-section">';
|
||||||
|
$out .= '<h2 class="civicinfra-record-heading">Group Order</h2>';
|
||||||
|
$out .= '<form method="post" action="' . z_root() . '/assoc_profile/manage">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="save_fields">';
|
||||||
|
$out .= '<div class="mb-3">';
|
||||||
|
$out .= '<label class="form-label" for="group-order">Groups <span class="civicinfra-help-text">(one per line, in display order)</span></label>';
|
||||||
|
$out .= '<textarea id="group-order" class="form-control form-control-sm" name="groups" rows="' . count($groups) . '" style="max-width:24rem;font-family:var(--bs-font-monospace)">'
|
||||||
|
. assoc_h(implode("\n", $groups)) . '</textarea>';
|
||||||
|
$out .= '</div>';
|
||||||
|
$out .= '<button type="submit" class="btn btn-primary btn-sm">Save Group Order</button>';
|
||||||
|
$out .= '</form></section>';
|
||||||
|
|
||||||
|
$out .= '</div>';
|
||||||
|
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 '<div class="civicinfra-notice civicinfra-warning">No matching associations found for export.</div>';
|
||||||
|
}
|
||||||
|
$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 = '<div class="assoc-manage">';
|
||||||
|
$out .= '<header class="civicinfra-manage-header">';
|
||||||
|
$out .= '<h1 class="civicinfra-manage-title">Import Associations</h1>';
|
||||||
|
$out .= '<div class="civicinfra-manage-actions">';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary btn-sm">← Back</a>';
|
||||||
|
$out .= '</div></header>';
|
||||||
|
|
||||||
|
$out .= '<div class="civicinfra-notice mb-4">';
|
||||||
|
$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 .= '</div>';
|
||||||
|
|
||||||
|
$out .= '<section class="civicinfra-form-section">';
|
||||||
|
$out .= '<form method="post" action="' . z_root() . '/assoc_profile/manage"
|
||||||
|
enctype="multipart/form-data">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="import_upload">';
|
||||||
|
$out .= '<div class="mb-3">';
|
||||||
|
$out .= '<label class="form-label" for="import_file">Association registry JSON file</label>';
|
||||||
|
$out .= '<input type="file" class="form-control form-control-sm" id="import_file"
|
||||||
|
name="import_file" accept=".json" required style="max-width:24rem;">';
|
||||||
|
$out .= '</div>';
|
||||||
|
$out .= '<button type="submit" class="btn btn-primary">Upload and Review</button>';
|
||||||
|
$out .= '</form></section></div>';
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// IMPORT — HANDLE UPLOAD AND PRODUCE DIFF
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function assoc_handle_import_upload() {
|
||||||
|
if (empty($_FILES['import_file']['tmp_name'])) {
|
||||||
|
return '<div class="civicinfra-notice civicinfra-warning">No file received.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = @file_get_contents($_FILES['import_file']['tmp_name']);
|
||||||
|
if (!$raw) {
|
||||||
|
return '<div class="civicinfra-notice civicinfra-warning">Could not read uploaded file.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$incoming = json_decode($raw, true);
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
return '<div class="civicinfra-notice civicinfra-warning">Invalid JSON in uploaded file.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 '<div class="civicinfra-notice">No changes found. The uploaded file matches the current registry.</div>'
|
||||||
|
. '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary btn-sm mt-3">← Back</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode the incoming data for the confirmation form
|
||||||
|
$encoded = base64_encode(json_encode($incoming));
|
||||||
|
|
||||||
|
$out = '<div class="assoc-manage">';
|
||||||
|
$out .= '<header class="civicinfra-manage-header">';
|
||||||
|
$out .= '<h1 class="civicinfra-manage-title">Review Import</h1>';
|
||||||
|
$out .= '</header>';
|
||||||
|
|
||||||
|
if (!empty($new)) {
|
||||||
|
$out .= '<section class="civicinfra-form-section mb-4">';
|
||||||
|
$out .= '<h2 class="civicinfra-record-heading">New — ' . count($new) . ' association' . (count($new) > 1 ? 's' : '') . '</h2>';
|
||||||
|
$out .= '<p class="civicinfra-help-text">These do not exist in the current registry and will be added.</p>';
|
||||||
|
$out .= '<ul class="civicinfra-channel-list">';
|
||||||
|
foreach ($new as $slug => $entry) {
|
||||||
|
$out .= '<li><span class="civicinfra-channel-label">' . assoc_h($entry['assoc_legal_name'] ?? $slug) . '</span>';
|
||||||
|
$out .= ' <code class="civicinfra-record-id">' . assoc_h($slug) . '</code></li>';
|
||||||
|
}
|
||||||
|
$out .= '</ul></section>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($changed)) {
|
||||||
|
$out .= '<section class="civicinfra-form-section mb-4">';
|
||||||
|
$out .= '<h2 class="civicinfra-record-heading">Changed — ' . count($changed) . ' association' . (count($changed) > 1 ? 's' : '') . '</h2>';
|
||||||
|
$out .= '<p class="civicinfra-help-text">These exist in the registry with different values. Choose to import or skip each one.</p>';
|
||||||
|
$out .= '<div id="assoc-diff-container">';
|
||||||
|
|
||||||
|
foreach ($changed as $slug => $data) {
|
||||||
|
$name = $data['current']['assoc_legal_name'] ?? $slug;
|
||||||
|
$out .= '<div class="assoc-diff-entry assoc-diff-entry-changed">';
|
||||||
|
$out .= '<div class="assoc-diff-entry-header">';
|
||||||
|
$out .= '<span class="civicinfra-channel-label">' . assoc_h($name) . '</span>';
|
||||||
|
$out .= '<code class="civicinfra-record-id">' . assoc_h($slug) . '</code>';
|
||||||
|
$out .= '<div class="d-flex gap-2 ms-auto">';
|
||||||
|
$out .= '<label class="d-flex align-items-center gap-1 fw-normal">
|
||||||
|
<input type="radio" name="diff_action[]" value="import" data-slug="' . assoc_h($slug) . '" checked> Import changes
|
||||||
|
</label>';
|
||||||
|
$out .= '<label class="d-flex align-items-center gap-1 fw-normal">
|
||||||
|
<input type="radio" name="diff_action[]" value="skip" data-slug="' . assoc_h($slug) . '"> Skip
|
||||||
|
</label>';
|
||||||
|
$out .= '</div></div>';
|
||||||
|
|
||||||
|
$out .= '<div class="civicinfra-diff-block">';
|
||||||
|
foreach ($data['diffs'] as $field => $diff) {
|
||||||
|
$out .= '<span class="civicinfra-diff-remove">' . assoc_h($field) . ': ' . assoc_h($diff['current']) . '</span>';
|
||||||
|
$out .= '<span class="civicinfra-diff-add">' . assoc_h($field) . ': ' . assoc_h($diff['incoming']) . '</span>';
|
||||||
|
}
|
||||||
|
$out .= '</div></div>';
|
||||||
|
}
|
||||||
|
$out .= '</div></section>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($same)) {
|
||||||
|
$out .= '<p class="civicinfra-record-meta mb-4">' . count($same) . ' association' . (count($same) > 1 ? 's' : '') . ' unchanged — skipped.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$out .= '<form method="post" action="' . z_root() . '/assoc_profile/manage">';
|
||||||
|
$out .= assoc_csrf_token();
|
||||||
|
$out .= '<input type="hidden" name="assoc_action" value="import_confirm">';
|
||||||
|
$out .= '<input type="hidden" name="import_data" value="' . assoc_h($encoded) . '">';
|
||||||
|
|
||||||
|
// Collect diff decisions
|
||||||
|
foreach ($changed as $slug => $data) {
|
||||||
|
$out .= '<input type="hidden" name="diff_decisions[' . assoc_h($slug) . ']" value="import" id="diff-decision-' . assoc_h($slug) . '">';
|
||||||
|
}
|
||||||
|
|
||||||
|
$out .= '<div class="d-flex gap-2 mt-2">';
|
||||||
|
$out .= '<button type="submit" class="btn btn-primary">Apply Import</button>';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage/import" class="btn btn-outline-secondary">Upload different file</a>';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary">Cancel</a>';
|
||||||
|
$out .= '</div></form></div>';
|
||||||
|
|
||||||
|
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 '<div class="civicinfra-notice civicinfra-warning">Import data missing or corrupted.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 '<div class="civicinfra-notice civicinfra-warning">Failed to write registry. Check server logs.</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = '<div class="civicinfra-notice">';
|
||||||
|
$out .= '<strong>Import complete.</strong> ';
|
||||||
|
if ($added) $out .= $added . ' added. ';
|
||||||
|
if ($updated) $out .= $updated . ' updated. ';
|
||||||
|
if ($skipped) $out .= $skipped . ' skipped.';
|
||||||
|
$out .= '</div>';
|
||||||
|
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary btn-sm mt-3">← Back to Associations</a>';
|
||||||
|
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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user