From dad39132e6e1c1ebde51023ce507ea7c7a2c8027 Mon Sep 17 00:00:00 2001 From: TheRON Date: Sat, 6 Jun 2026 09:16:49 -0400 Subject: [PATCH] Updated --- .../addon/assoc_profile/assoc_profile.php | 58 +- .../assoc_profile/assoc_profile_manage.php | 611 +++++++++++++++--- .../addon/assoc_profile/mod_assoc_profile.pdl | 9 + 3 files changed, 581 insertions(+), 97 deletions(-) diff --git a/hubzilla/addon/assoc_profile/assoc_profile.php b/hubzilla/addon/assoc_profile/assoc_profile.php index 7784179..6daf5f4 100644 --- a/hubzilla/addon/assoc_profile/assoc_profile.php +++ b/hubzilla/addon/assoc_profile/assoc_profile.php @@ -150,6 +150,9 @@ function assoc_profile_content() { if (function_exists('head_add_css')) { head_add_css('/addon/assoc_profile/view/css/assoc_profile.css'); } + if (function_exists('head_add_js')) { + head_add_js('/addon/assoc_profile/view/js/assoc_profile.js'); + } $action = argv(1) ?? ''; @@ -158,29 +161,28 @@ function assoc_profile_content() { } if (!assoc_is_operator()) { - return '
Operator access required.
'; + return '
Operator access required.
'; } - $sub = argv(2) ?? ''; - $slug = argv(3) ?? ''; + $sub = argv(2) ?? ''; + $slug = argv(3) ?? ''; if ($_SERVER['REQUEST_METHOD'] === 'POST') { return assoc_handle_post(); } - // Manage index if (!$sub) return assoc_render_manage_index(); - // Association actions if ($sub === 'assoc') { if (!$slug) return assoc_render_add_association_form(); return assoc_render_edit_association_form($slug); } - // Field actions if ($sub === 'fields') return assoc_render_fields_form(); - return '
Unknown management action.
'; + if ($sub === 'import') return assoc_render_import_form(); + + return '
Unknown management action.
'; } // ---------------------------------------------------------------------------- @@ -189,19 +191,23 @@ function assoc_profile_content() { function assoc_handle_post() { if (!assoc_verify_csrf()) { - return '
Invalid form token.
'; + return '
Invalid form token.
'; } $action = $_POST['assoc_action'] ?? ''; switch ($action) { - case 'save_association': return assoc_save_association(); - case 'add_association': return assoc_add_association(); - case 'save_fields': return assoc_save_fields(); - case 'add_field': return assoc_add_field(); - case 'remove_field': return assoc_remove_field(); + case 'save_association': return assoc_save_association(); + case 'add_association': return assoc_add_association(); + case 'delete_association':return assoc_delete_association(); + case 'save_fields': return assoc_save_fields(); + case 'add_field': return assoc_add_field(); + case 'remove_field': return assoc_remove_field(); + case 'export_selected': return assoc_handle_export($_POST['export_slugs'] ?? ''); + case 'import_upload': return assoc_handle_import_upload(); + case 'import_confirm': return assoc_handle_import_confirm(); default: - return '
Unknown action.
'; + return '
Unknown action.
'; } } @@ -256,7 +262,29 @@ function assoc_add_association() { if (assoc_write_registry($registry)) { goaway(z_root() . '/assoc_profile/manage/assoc/' . $slug); } - return '
Failed to create association.
'; + return '
Failed to create association.
'; +} + +// ---------------------------------------------------------------------------- +// DELETE ASSOCIATION +// ---------------------------------------------------------------------------- + +function assoc_delete_association() { + $slug = substr(strip_tags($_POST['assoc_slug'] ?? ''), 0, 128); + if (!$slug) return '
Missing slug.
'; + + $registry = assoc_load_registry(); + if (!isset($registry[$slug])) { + return '
Association not found.
'; + } + + $name = $registry[$slug]['assoc_legal_name'] ?? $slug; + unset($registry[$slug]); + + if (assoc_write_registry($registry)) { + goaway(z_root() . '/assoc_profile/manage'); + } + return '
Delete failed. Check server logs.
'; } // ---------------------------------------------------------------------------- diff --git a/hubzilla/addon/assoc_profile/assoc_profile_manage.php b/hubzilla/addon/assoc_profile/assoc_profile_manage.php index af8cf19..bfc04bc 100644 --- a/hubzilla/addon/assoc_profile/assoc_profile_manage.php +++ b/hubzilla/addon/assoc_profile/assoc_profile_manage.php @@ -2,72 +2,201 @@ /** * Association Profile — Management Renderer - * Render functions for /assoc_profile/manage interface. * 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(); - $out = '
'; - $out .= '

Association Profile — Management

'; - $out .= '
'; - $out .= 'Add Association'; - $out .= 'Manage Fields'; - $out .= '
'; - if (empty($registry)) { - $out .= '

No associations registered.

'; - } else { - $out .= ''; - $out .= ''; - foreach ($registry as $slug => $entry) { - $out .= ''; - $out .= ''; - $out .= ''; - $out .= ''; - $out .= ''; - $out .= ''; - $out .= ''; - $out .= ''; - } - $out .= '
SlugLegal NameTypeUnitsUpdated
' . assoc_h($slug) . '' . assoc_h($entry['assoc_legal_name'] ?? '—') . '' . assoc_h($entry['assoc_type'] ?? '—') . '' . assoc_h($entry['assoc_units'] ?? '—') . '' . assoc_h($entry['assoc_updated'] ?? '—') . 'Edit
'; + $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 = '
'; + + // 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 .= '

Add Association

'; + $out .= '
'; + $out .= '

Add Association

'; + $out .= '
'; + $out .= '← Back'; + $out .= '
'; + + $out .= '
'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; $out .= '
'; - $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 .= 'Cancel'; - $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; + 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 .= '

' . assoc_h($entry['assoc_legal_name'] ?? $slug) . '

'; - $out .= '

' . assoc_h($slug) . '

'; + $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 .= ''; @@ -76,7 +205,10 @@ function assoc_render_edit_association_form($slug) { foreach ($groups as $group) { $group_fields = $by_group[$group] ?? []; if (empty($group_fields)) continue; - $out .= '
' . assoc_h($group) . '
'; + + $out .= '
'; + $out .= '

' . assoc_h($group) . '

'; + foreach ($group_fields as $f) { $nick = $f['nickname']; $val = $entry[$nick] ?? ($f['default'] ?? ''); @@ -85,19 +217,20 @@ function assoc_render_edit_association_form($slug) { $help = $f['help'] ?? ''; if ($type === 'readonly') { - $out .= '
'; - $out .= ''; - $out .= '
' . assoc_h($val ?: '—') . '
'; + $out .= '
'; + $out .= '

' . assoc_h($label) . '

'; + $out .= '

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

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

' . assoc_h($help) . '

'; if ($type === 'select' && !empty($f['options'])) { - $out .= ''; foreach ($f['options'] as $opt) { $sel = ($val === $opt) ? 'selected' : ''; $disp = ($opt === 'UNK') ? 'Unknown — not yet verified' : $opt; @@ -105,94 +238,408 @@ function assoc_render_edit_association_form($slug) { } $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 .= ''; + value="' . assoc_h($val) . '" + style="max-width:' . $width . ';">'; } $out .= '
'; } + $out .= '
'; } - $out .= '
'; - $out .= ' '; - $out .= 'Back'; - $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 .= '

Manage Fields

'; - $out .= '

Changes to labels and groups take effect immediately. Adding a field backfills all existing associations with the default value. Removing a field deletes its data from all associations — this cannot be undone.

'; + $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 .= '
'; + $out .= '
NicknameLabelTypeGroupHelp
'; + $out .= ' + + + + + + + '; + foreach ($fields as $f) { $nick = $f['nickname']; + $type = $f['type'] ?? 'text'; $out .= ''; - $out .= ''; - $out .= ''; - $out .= ''; - $out .= ''; - $out .= ''; + $out .= ''; + $out .= ''; + $out .= ''; + $out .= ''; + $out .= ''; $out .= ''; } - $out .= '
NicknameLabelTypeGroupHelp text
' . assoc_h($nick) . '' . assoc_h($f['type'] ?? 'text') . '' . assoc_h($nick) . '' . assoc_h($type) . ''; - if (($f['type'] ?? '') !== 'readonly') { + if ($type !== 'readonly') { $out .= ''; + onclick="return confirm(\'Remove ' . assoc_h($nick) . ' from ALL associations? Cannot be undone.\')">Remove'; } $out .= '
'; - $out .= ''; - $out .= '
'; + $out .= '
'; + $out .= '
'; + $out .= ''; + // Hidden remove forms foreach ($fields as $f) { if (($f['type'] ?? '') === 'readonly') continue; $nick = $f['nickname']; - $out .= '
'; + $out .= ''; $out .= assoc_csrf_token(); $out .= ''; $out .= ''; $out .= '
'; } - $out .= '
Add New Field
'; + // Add new field + $out .= '
'; + $out .= '

Add New Field

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

Group Order

'; $out .= '
'; $out .= assoc_csrf_token(); $out .= ''; - $out .= '
'; - $out .= '
'; + $out .= '
'; + $out .= ''; + $out .= ''; + $out .= '
'; $out .= ''; - $out .= '
'; + $out .= '
'; - $out .= '
Back
'; $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 .= '
    '; + foreach ($new as $slug => $entry) { + $out .= '
  • ' . assoc_h($entry['assoc_legal_name'] ?? $slug) . ''; + $out .= ' ' . assoc_h($slug) . '
  • '; + } + $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; +} diff --git a/hubzilla/addon/assoc_profile/mod_assoc_profile.pdl b/hubzilla/addon/assoc_profile/mod_assoc_profile.pdl index 4bf8ec7..2bb989c 100644 --- a/hubzilla/addon/assoc_profile/mod_assoc_profile.pdl +++ b/hubzilla/addon/assoc_profile/mod_assoc_profile.pdl @@ -1,5 +1,14 @@ [template]default[/template] +[region=aside] +[widget=vs01][/widget] +[/region] + [region=content] $content [/region] + +[region=right_aside] +[widget=notifications][/widget] +[widget=newmember][/widget] +[/region]