diff --git a/hubzilla/addon/assoc_profile/view/css/assoc_profile_manage.php b/hubzilla/addon/assoc_profile/view/css/assoc_profile_manage.php new file mode 100644 index 0000000..3637599 --- /dev/null +++ b/hubzilla/addon/assoc_profile/view/css/assoc_profile_manage.php @@ -0,0 +1,688 @@ + $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 — single horizontal row, no wrap + $out .= ''; + + // Bulk bar (hidden until selection) + $out .= ''; + + // Hidden export form + $out .= ''; + + // Table + $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 .= '
SlugLegal NameUnitsUpdated
' + . '' . assoc_h($slug) . '' + . '' . assoc_h($entry['assoc_legal_name'] ?? '—') . '' . assoc_h($entry['assoc_units'] ?? '—') . '' . 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; + + // 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 = '
'; + $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 .= ''; + + // Tab nav + $out .= ''; + + // Tab panes + $out .= '
'; + $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 .= '
'; + + 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 { + $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 .= ''; + } + $out .= '
'; + } + + $out .= '
'; // tab-pane + $first = false; + } + $out .= '
'; // tab-content + + // Save bar below tabs + $out .= '
'; + $out .= ' '; + $out .= 'Cancel'; + $out .= '
'; + $out .= '
'; + + // Delete section + $out .= '
'; + $out .= '
Delete Association
'; + $out .= '

Permanently removes ' . assoc_h($name) . ' from the registry. '; + $out .= 'You must also remove the 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 .= '
NicknameLabelTypeGroupHelp 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; +}