Updated
This commit is contained in:
269
hubzilla/addon/assoc_profile/assoc_fields.json.template
Normal file
269
hubzilla/addon/assoc_profile/assoc_fields.json.template
Normal file
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"_note": "Copy to assoc_fields.json on the host. Do not commit assoc_fields.json. This file is the canonical field definition for all association registry entries. Adding a field here and saving via the management interface will backfill all existing associations with the default value.",
|
||||
"_version": "1.0",
|
||||
"groups": [
|
||||
"Identity",
|
||||
"Physical Structure",
|
||||
"Governance and Management",
|
||||
"Legal and Compliance",
|
||||
"Record"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"nickname": "assoc_legal_name",
|
||||
"label": "Legal Name",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "",
|
||||
"required": true,
|
||||
"help": "Full legal name as recorded with Illinois Secretary of State"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_type",
|
||||
"label": "Association Type",
|
||||
"type": "select",
|
||||
"group": "Identity",
|
||||
"default": "UNK",
|
||||
"options": ["Condominium", "Master", "CIC", "Unincorporated", "UNK", "Disputed"],
|
||||
"required": true,
|
||||
"help": "VS-01 verified value"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_statute",
|
||||
"label": "Governing Statute",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "e.g. 765 ILCS 605 — populated after VS-01 verification"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_street",
|
||||
"label": "Street Address",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "Physical address of the property"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_city",
|
||||
"label": "City",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": ""
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_county",
|
||||
"label": "County",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "e.g. Cook County"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_state",
|
||||
"label": "State",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "IL",
|
||||
"required": false,
|
||||
"help": ""
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_zip",
|
||||
"label": "ZIP Code",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": ""
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_placekey",
|
||||
"label": "Placekey",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "Permanent geographic anchor — e.g. 247-222@5sb-8bs-y35"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_website",
|
||||
"label": "Association Website",
|
||||
"type": "text",
|
||||
"group": "Identity",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "URL of association-managed site if any"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_buildings",
|
||||
"label": "Number of Buildings",
|
||||
"type": "text",
|
||||
"group": "Physical Structure",
|
||||
"default": "UNK",
|
||||
"required": false,
|
||||
"help": "Integer or UNK — VS-02"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_units",
|
||||
"label": "Number of Units",
|
||||
"type": "text",
|
||||
"group": "Physical Structure",
|
||||
"default": "UNK",
|
||||
"required": false,
|
||||
"help": "Integer or UNK — VS-02"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_year_built",
|
||||
"label": "Year Built",
|
||||
"type": "text",
|
||||
"group": "Physical Structure",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "e.g. 1974"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_stories",
|
||||
"label": "Stories Per Building",
|
||||
"type": "text",
|
||||
"group": "Physical Structure",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "e.g. 3"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_mgmt_company",
|
||||
"label": "Management Company",
|
||||
"type": "text",
|
||||
"group": "Governance and Management",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": ""
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_mgmt_contact",
|
||||
"label": "Management Contact",
|
||||
"type": "text",
|
||||
"group": "Governance and Management",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "Name and email of primary contact"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_mgmt_certified",
|
||||
"label": "Manager IDFPR Certified",
|
||||
"type": "select",
|
||||
"group": "Governance and Management",
|
||||
"default": "UNK",
|
||||
"options": ["Yes", "No", "UNK"],
|
||||
"required": false,
|
||||
"help": "VS-03"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_cai_listing",
|
||||
"label": "CAI Directory Listing",
|
||||
"type": "text",
|
||||
"group": "Governance and Management",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "How the association or its management company appears in CAI — member, vendor, AAMC, etc."
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_bbb_standing",
|
||||
"label": "BBB Standing",
|
||||
"type": "text",
|
||||
"group": "Governance and Management",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "Better Business Bureau rating and status for the management company"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_idfpr_complaint",
|
||||
"label": "IDFPR Complaints",
|
||||
"type": "text",
|
||||
"group": "Governance and Management",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "Any complaints on record against the manager at IDFPR"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_sos_id",
|
||||
"label": "SOS Entity File Number",
|
||||
"type": "text",
|
||||
"group": "Legal and Compliance",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "Illinois Secretary of State entity file number — VS-08"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_sos_standing",
|
||||
"label": "Corporate Standing",
|
||||
"type": "select",
|
||||
"group": "Legal and Compliance",
|
||||
"default": "UNK",
|
||||
"options": ["Active", "Dissolved", "UNK"],
|
||||
"required": false,
|
||||
"help": "VS-08"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_declaration_recorded",
|
||||
"label": "Declaration Recorded",
|
||||
"type": "select",
|
||||
"group": "Legal and Compliance",
|
||||
"default": "UNK",
|
||||
"options": ["Yes", "No", "UNK"],
|
||||
"required": false,
|
||||
"help": "VS-10"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_declaration_instrument",
|
||||
"label": "Declaration Instrument No.",
|
||||
"type": "text",
|
||||
"group": "Legal and Compliance",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "County Recorder document number — VS-10"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_liens_active",
|
||||
"label": "Active Liens",
|
||||
"type": "text",
|
||||
"group": "Legal and Compliance",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "Count or description of active liens recorded against units or the association"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_litigation_active",
|
||||
"label": "Active Litigation",
|
||||
"type": "text",
|
||||
"group": "Legal and Compliance",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "Known active cases from public court record"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_registered",
|
||||
"label": "Registered Date",
|
||||
"type": "readonly",
|
||||
"group": "Record",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "ISO date when this association was added to the registry — set automatically"
|
||||
},
|
||||
{
|
||||
"nickname": "assoc_updated",
|
||||
"label": "Last Updated",
|
||||
"type": "readonly",
|
||||
"group": "Record",
|
||||
"default": "",
|
||||
"required": false,
|
||||
"help": "ISO date of most recent update — set automatically"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,147 +3,356 @@
|
||||
/**
|
||||
* Name: Association Profile
|
||||
* Description: Structured diagnostic identity fields for HOA association channels.
|
||||
* Version: 0.1.0
|
||||
* Version: 0.2.0
|
||||
* MinVersion: 11.0
|
||||
* MaxVersion: 12.0
|
||||
*/
|
||||
|
||||
function assoc_profile_module() {}
|
||||
|
||||
require_once 'addon/assoc_profile/assoc_profile_manage.php';
|
||||
|
||||
function assoc_profile_load() {
|
||||
register_hook('profile_edit', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_edit_hook');
|
||||
register_hook('profile_advanced', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_view_hook');
|
||||
register_hook('profile_edit', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_edit_hook');
|
||||
}
|
||||
|
||||
function assoc_profile_unload() {
|
||||
unregister_hook('profile_edit', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_edit_hook');
|
||||
unregister_hook('profile_advanced', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_view_hook');
|
||||
unregister_hook('profile_edit', 'addon/assoc_profile/assoc_profile.php', 'assoc_profile_edit_hook');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// HELPERS
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_h($value) {
|
||||
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
|
||||
function assoc_h($v) {
|
||||
return htmlspecialchars((string)$v, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
|
||||
function assoc_slug_from_address($channel_address) {
|
||||
// channel_address is "slug@node" — extract the slug
|
||||
$parts = explode('@', $channel_address);
|
||||
return $parts[0] ?? '';
|
||||
function assoc_slug_from_address($addr) {
|
||||
return explode('@', $addr)[0] ?? '';
|
||||
}
|
||||
|
||||
function assoc_is_operator() {
|
||||
if (!local_channel()) return false;
|
||||
$channel = App::get_channel();
|
||||
return local_channel() === intval($channel['channel_id']);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// REGISTRY I/O
|
||||
// FILE I/O
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_load_config() {
|
||||
$path = 'addon/assoc_profile/config.json';
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false) return [];
|
||||
$data = json_decode($raw, true);
|
||||
return (json_last_error() === JSON_ERROR_NONE) ? $data : [];
|
||||
$raw = @file_get_contents('addon/assoc_profile/config.json');
|
||||
if (!$raw) return [];
|
||||
$d = json_decode($raw, true);
|
||||
return json_last_error() === JSON_ERROR_NONE ? $d : [];
|
||||
}
|
||||
|
||||
function assoc_registry_path() {
|
||||
$c = assoc_load_config();
|
||||
return $c['registry_file'] ?? 'addon/assoc_profile/assoc_registry.json';
|
||||
}
|
||||
|
||||
function assoc_fields_path() {
|
||||
$c = assoc_load_config();
|
||||
return $c['fields_file'] ?? 'addon/assoc_profile/assoc_fields.json';
|
||||
}
|
||||
|
||||
function assoc_load_registry() {
|
||||
$config = assoc_load_config();
|
||||
$path = $config['registry_file']
|
||||
?? 'addon/assoc_profile/assoc_registry.json';
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false) {
|
||||
logger('assoc_profile: registry file not found: ' . $path);
|
||||
return [];
|
||||
}
|
||||
$data = json_decode($raw, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
logger('assoc_profile: registry file malformed: ' . $path);
|
||||
return [];
|
||||
}
|
||||
// Strip meta keys
|
||||
foreach (['_note','_version','_slug_format','_select_values'] as $k) {
|
||||
unset($data[$k]);
|
||||
}
|
||||
return $data;
|
||||
$raw = @file_get_contents(assoc_registry_path());
|
||||
if (!$raw) { logger('assoc_profile: registry not found'); return []; }
|
||||
$d = json_decode($raw, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) { logger('assoc_profile: registry malformed'); return []; }
|
||||
foreach (['_note','_version','_slug_format','_select_values'] as $k) unset($d[$k]);
|
||||
return $d;
|
||||
}
|
||||
|
||||
function assoc_write_registry($data) {
|
||||
$config = assoc_load_config();
|
||||
$path = $config['registry_file']
|
||||
?? 'addon/assoc_profile/assoc_registry.json';
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
if ($json === false) {
|
||||
logger('assoc_profile: failed to encode registry');
|
||||
return false;
|
||||
}
|
||||
$result = file_put_contents($path, $json, LOCK_EX);
|
||||
if ($result === false) {
|
||||
logger('assoc_profile: failed to write registry: ' . $path);
|
||||
return false;
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
if (!$json) { logger('assoc_profile: encode failed'); return false; }
|
||||
if (file_put_contents(assoc_registry_path(), $json, LOCK_EX) === false) {
|
||||
logger('assoc_profile: write failed'); return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function assoc_load_fields() {
|
||||
$raw = @file_get_contents(assoc_fields_path());
|
||||
if (!$raw) { logger('assoc_profile: fields file not found'); return ['groups'=>[],'fields'=>[]]; }
|
||||
$d = json_decode($raw, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) { logger('assoc_profile: fields file malformed'); return ['groups'=>[],'fields'=>[]]; }
|
||||
return $d;
|
||||
}
|
||||
|
||||
function assoc_write_fields($data) {
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
||||
if (!$json) return false;
|
||||
return file_put_contents(assoc_fields_path(), $json, LOCK_EX) !== false;
|
||||
}
|
||||
|
||||
function assoc_get($slug) {
|
||||
$r = assoc_load_registry();
|
||||
return $r[$slug] ?? null;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// FIELD HELPERS
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_field_map() {
|
||||
$def = assoc_load_fields();
|
||||
$map = [];
|
||||
foreach ($def['fields'] ?? [] as $f) {
|
||||
$map[$f['nickname']] = $f;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
function assoc_blank_entry() {
|
||||
$map = assoc_field_map();
|
||||
$entry = [];
|
||||
foreach ($map as $nick => $f) {
|
||||
$entry[$nick] = $f['default'] ?? '';
|
||||
}
|
||||
return $entry;
|
||||
}
|
||||
|
||||
function assoc_backfill_all($new_field_def) {
|
||||
$registry = assoc_load_registry();
|
||||
return $registry[$slug] ?? null;
|
||||
$nick = $new_field_def['nickname'];
|
||||
$default = $new_field_def['default'] ?? '';
|
||||
foreach ($registry as $slug => &$entry) {
|
||||
if (!isset($entry[$nick])) {
|
||||
$entry[$nick] = $default;
|
||||
}
|
||||
}
|
||||
unset($entry);
|
||||
return $registry;
|
||||
}
|
||||
|
||||
function assoc_remove_field_from_all($nickname) {
|
||||
$registry = assoc_load_registry();
|
||||
foreach ($registry as $slug => &$entry) {
|
||||
unset($entry[$nickname]);
|
||||
}
|
||||
unset($entry);
|
||||
return $registry;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// VALIDATION
|
||||
// CONTENT ROUTER
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_allowed_values() {
|
||||
return [
|
||||
'assoc_type' => ['Condominium','Master','CIC','Unincorporated','UNK','Disputed'],
|
||||
'assoc_mgmt_certified' => ['Yes','No','UNK'],
|
||||
'assoc_sos_standing' => ['Active','Dissolved','UNK'],
|
||||
'assoc_declaration_recorded' => ['Yes','No','UNK'],
|
||||
];
|
||||
}
|
||||
|
||||
function assoc_validate_select($field, $value) {
|
||||
$allowed = assoc_allowed_values();
|
||||
if (!isset($allowed[$field])) return true;
|
||||
return in_array($value, $allowed[$field], true);
|
||||
}
|
||||
|
||||
function assoc_field_names() {
|
||||
return [
|
||||
'assoc_legal_name', 'assoc_type', 'assoc_statute',
|
||||
'assoc_street', 'assoc_city', 'assoc_county', 'assoc_state',
|
||||
'assoc_zip', 'assoc_placekey', 'assoc_website',
|
||||
'assoc_buildings', 'assoc_units', 'assoc_year_built', 'assoc_stories',
|
||||
'assoc_mgmt_company', 'assoc_mgmt_contact', 'assoc_mgmt_certified',
|
||||
'assoc_cai_listing',
|
||||
'assoc_sos_id', 'assoc_sos_standing',
|
||||
'assoc_declaration_recorded', 'assoc_declaration_instrument',
|
||||
'assoc_registered', 'assoc_updated',
|
||||
];
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// PROFILE EDIT HOOK
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_profile_edit_hook(&$b) {
|
||||
if (!local_channel()) return;
|
||||
|
||||
$channel = App::get_channel();
|
||||
$slug = assoc_slug_from_address($channel['channel_address'] ?? '');
|
||||
if (!$slug) return;
|
||||
|
||||
$entry = assoc_get($slug);
|
||||
if (!$entry) return;
|
||||
|
||||
// Only channel owner may edit
|
||||
if (local_channel() !== intval($channel['channel_id'])) return;
|
||||
|
||||
function assoc_profile_content() {
|
||||
if (function_exists('head_add_css')) {
|
||||
head_add_css('/addon/assoc_profile/view/css/assoc_profile.css');
|
||||
}
|
||||
|
||||
$b['html'] .= assoc_render_edit_form($slug, $entry);
|
||||
$action = argv(1) ?? '';
|
||||
|
||||
if ($action !== 'manage') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!assoc_is_operator()) {
|
||||
return '<div class="alert alert-danger">Operator access required.</div>';
|
||||
}
|
||||
|
||||
$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 '<div class="alert alert-warning">Unknown management action.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// POST HANDLER
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_handle_post() {
|
||||
if (!assoc_verify_csrf()) {
|
||||
return '<div class="alert alert-danger">Invalid form token.</div>';
|
||||
}
|
||||
|
||||
$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();
|
||||
default:
|
||||
return '<div class="alert alert-danger">Unknown action.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SAVE ASSOCIATION
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_save_association() {
|
||||
$slug = substr(strip_tags($_POST['assoc_slug'] ?? ''), 0, 128);
|
||||
if (!$slug) return '<div class="alert alert-danger">Missing slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (!isset($registry[$slug])) {
|
||||
return '<div class="alert alert-danger">Association not found.</div>';
|
||||
}
|
||||
|
||||
$field_map = assoc_field_map();
|
||||
foreach ($field_map as $nick => $fdef) {
|
||||
if (($fdef['type'] ?? '') === 'readonly') continue;
|
||||
if (!isset($_POST[$nick])) continue;
|
||||
$val = substr(strip_tags((string)$_POST[$nick]), 0, 1024);
|
||||
if (isset($fdef['options']) && !in_array($val, $fdef['options'], true)) continue;
|
||||
$registry[$slug][$nick] = $val;
|
||||
}
|
||||
$registry[$slug]['assoc_updated'] = date('Y-m-d');
|
||||
|
||||
if (assoc_write_registry($registry)) {
|
||||
goaway(z_root() . '/assoc_profile/manage/assoc/' . $slug);
|
||||
}
|
||||
return '<div class="alert alert-danger">Save failed. Check server logs.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ADD ASSOCIATION
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_add_association() {
|
||||
$slug = strtolower(trim($_POST['new_slug'] ?? ''));
|
||||
$slug = preg_replace('/[^a-z0-9\-]/', '', $slug);
|
||||
if (!$slug) return '<div class="alert alert-danger">Invalid slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (isset($registry[$slug])) {
|
||||
return '<div class="alert alert-warning">Slug already exists: ' . assoc_h($slug) . '</div>';
|
||||
}
|
||||
|
||||
$entry = assoc_blank_entry();
|
||||
$entry['assoc_registered'] = date('Y-m-d');
|
||||
$entry['assoc_updated'] = date('Y-m-d');
|
||||
$registry[$slug] = $entry;
|
||||
|
||||
if (assoc_write_registry($registry)) {
|
||||
goaway(z_root() . '/assoc_profile/manage/assoc/' . $slug);
|
||||
}
|
||||
return '<div class="alert alert-danger">Failed to create association.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// FIELD MANAGEMENT
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_save_fields() {
|
||||
// Save label and group changes to existing fields
|
||||
$def = assoc_load_fields();
|
||||
$fields = &$def['fields'];
|
||||
|
||||
foreach ($fields as &$f) {
|
||||
$nick = $f['nickname'];
|
||||
if (isset($_POST['label_' . $nick])) {
|
||||
$f['label'] = substr(strip_tags($_POST['label_' . $nick]), 0, 128);
|
||||
}
|
||||
if (isset($_POST['group_' . $nick])) {
|
||||
$f['group'] = substr(strip_tags($_POST['group_' . $nick]), 0, 64);
|
||||
}
|
||||
if (isset($_POST['help_' . $nick])) {
|
||||
$f['help'] = substr(strip_tags($_POST['help_' . $nick]), 0, 256);
|
||||
}
|
||||
}
|
||||
unset($f);
|
||||
|
||||
// Save group order
|
||||
if (!empty($_POST['groups'])) {
|
||||
$groups = array_map(function($g) { return substr(strip_tags($g), 0, 64); },
|
||||
explode("\n", $_POST['groups']));
|
||||
$def['groups'] = array_values(array_filter(array_map('trim', $groups)));
|
||||
}
|
||||
|
||||
if (assoc_write_fields($def)) {
|
||||
goaway(z_root() . '/assoc_profile/manage/fields');
|
||||
}
|
||||
return '<div class="alert alert-danger">Save failed.</div>';
|
||||
}
|
||||
|
||||
function assoc_add_field() {
|
||||
$nick = strtolower(preg_replace('/[^a-z0-9_]/', '', $_POST['new_nickname'] ?? ''));
|
||||
$label = substr(strip_tags($_POST['new_label'] ?? ''), 0, 128);
|
||||
$type = in_array($_POST['new_type'] ?? '', ['text','select','readonly']) ? $_POST['new_type'] : 'text';
|
||||
$group = substr(strip_tags($_POST['new_group'] ?? ''), 0, 64);
|
||||
$default = substr(strip_tags($_POST['new_default'] ?? ''), 0, 128);
|
||||
$help = substr(strip_tags($_POST['new_help'] ?? ''), 0, 256);
|
||||
|
||||
if (!$nick || !$label) {
|
||||
return '<div class="alert alert-danger">Nickname and label are required.</div>';
|
||||
}
|
||||
|
||||
$def = assoc_load_fields();
|
||||
foreach ($def['fields'] as $f) {
|
||||
if ($f['nickname'] === $nick) {
|
||||
return '<div class="alert alert-warning">Field already exists: ' . assoc_h($nick) . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
$new_field = [
|
||||
'nickname' => $nick,
|
||||
'label' => $label,
|
||||
'type' => $type,
|
||||
'group' => $group,
|
||||
'default' => $default,
|
||||
'required' => false,
|
||||
'help' => $help,
|
||||
];
|
||||
|
||||
if ($type === 'select' && !empty($_POST['new_options'])) {
|
||||
$opts = array_map('trim', explode("\n", $_POST['new_options']));
|
||||
$new_field['options'] = array_values(array_filter($opts));
|
||||
}
|
||||
|
||||
$def['fields'][] = $new_field;
|
||||
assoc_write_fields($def);
|
||||
|
||||
// Backfill all existing associations
|
||||
$registry = assoc_backfill_all($new_field);
|
||||
assoc_write_registry($registry);
|
||||
|
||||
goaway(z_root() . '/assoc_profile/manage/fields');
|
||||
}
|
||||
|
||||
function assoc_remove_field() {
|
||||
$nick = preg_replace('/[^a-z0-9_]/', '', $_POST['remove_nickname'] ?? '');
|
||||
if (!$nick) return '<div class="alert alert-danger">Missing nickname.</div>';
|
||||
|
||||
$def = assoc_load_fields();
|
||||
$def['fields'] = array_values(array_filter($def['fields'], function($f) use ($nick) {
|
||||
return $f['nickname'] !== $nick;
|
||||
}));
|
||||
assoc_write_fields($def);
|
||||
|
||||
$registry = assoc_remove_field_from_all($nick);
|
||||
assoc_write_registry($registry);
|
||||
|
||||
goaway(z_root() . '/assoc_profile/manage/fields');
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -153,215 +362,70 @@ function assoc_profile_edit_hook(&$b) {
|
||||
function assoc_profile_view_hook(&$o) {
|
||||
$uid = intval(App::$profile['profile_uid'] ?? 0);
|
||||
if (!$uid) return;
|
||||
|
||||
$r = q("SELECT channel_address FROM channel WHERE channel_id = %d LIMIT 1",
|
||||
intval($uid));
|
||||
$r = q("SELECT channel_address FROM channel WHERE channel_id = %d LIMIT 1", intval($uid));
|
||||
if (!$r) return;
|
||||
|
||||
$slug = assoc_slug_from_address($r[0]['channel_address']);
|
||||
$entry = assoc_get($slug);
|
||||
if (!$entry) return;
|
||||
|
||||
if (function_exists('head_add_css')) {
|
||||
head_add_css('/addon/assoc_profile/view/css/assoc_profile.css');
|
||||
}
|
||||
|
||||
if (function_exists('head_add_css')) head_add_css('/addon/assoc_profile/view/css/assoc_profile.css');
|
||||
$o .= assoc_render_view($entry);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// RENDER — VIEW
|
||||
// PROFILE EDIT HOOK
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_profile_edit_hook(&$b) {
|
||||
if (!assoc_is_operator()) return;
|
||||
$channel = App::get_channel();
|
||||
$slug = assoc_slug_from_address($channel['channel_address'] ?? '');
|
||||
$entry = assoc_get($slug);
|
||||
if (!$entry) return;
|
||||
if (function_exists('head_add_css')) head_add_css('/addon/assoc_profile/view/css/assoc_profile.css');
|
||||
$b['html'] .= '<div class="assoc-profile-edit-link mt-2">
|
||||
<a href="' . z_root() . '/assoc_profile/manage/assoc/' . assoc_h($slug) . '" class="btn btn-sm btn-outline-secondary">
|
||||
Edit Association Diagnostic Profile
|
||||
</a></div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// RENDER — PROFILE VIEW
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_render_view($entry) {
|
||||
$labels = assoc_field_labels();
|
||||
$groups = assoc_field_groups();
|
||||
$def = assoc_load_fields();
|
||||
$groups = $def['groups'] ?? [];
|
||||
$fields = $def['fields'] ?? [];
|
||||
|
||||
$by_group = [];
|
||||
foreach ($fields as $f) {
|
||||
$by_group[$f['group']][] = $f;
|
||||
}
|
||||
|
||||
$out = '<div class="assoc-profile-view">';
|
||||
$out .= '<h4 class="assoc-profile-heading">Association Diagnostic Profile</h4>';
|
||||
|
||||
foreach ($groups as $group_label => $fields) {
|
||||
foreach ($groups as $group) {
|
||||
$group_fields = $by_group[$group] ?? [];
|
||||
if (empty($group_fields)) continue;
|
||||
$out .= '<div class="assoc-profile-group">';
|
||||
$out .= '<h5 class="assoc-group-label">' . assoc_h($group_label) . '</h5>';
|
||||
foreach ($fields as $field) {
|
||||
$val = $entry[$field] ?? '';
|
||||
$out .= '<h5 class="assoc-group-label">' . assoc_h($group) . '</h5>';
|
||||
foreach ($group_fields as $f) {
|
||||
$nick = $f['nickname'];
|
||||
$val = $entry[$nick] ?? '';
|
||||
if ($val === '') $val = '—';
|
||||
if (in_array($val, ['UNK'])) $val = 'Unknown — not yet verified';
|
||||
if ($val === 'UNK') $val = 'Unknown — not yet verified';
|
||||
$out .= '<div class="assoc-profile-field">';
|
||||
$out .= '<span class="assoc-field-label">' . assoc_h($labels[$field] ?? $field) . ':</span> ';
|
||||
$out .= '<span class="assoc-field-label">' . assoc_h($f['label'] ?? $nick) . ':</span> ';
|
||||
$out .= '<span class="assoc-field-value">' . assoc_h($val) . '</span>';
|
||||
$out .= '</div>';
|
||||
}
|
||||
$out .= '</div>';
|
||||
}
|
||||
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// RENDER — EDIT FORM
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_render_edit_form($slug, $entry) {
|
||||
$labels = assoc_field_labels();
|
||||
$groups = assoc_field_groups();
|
||||
$allowed = assoc_allowed_values();
|
||||
|
||||
$out = '<div class="assoc-profile-edit">';
|
||||
$out .= '<h4 class="assoc-profile-heading">Association Diagnostic Profile</h4>';
|
||||
$out .= '<form method="post" action="/assoc_profile/save" class="assoc-edit-form">';
|
||||
$out .= '<input type="hidden" name="assoc_slug" value="' . assoc_h($slug) . '">';
|
||||
$out .= assoc_csrf_token();
|
||||
|
||||
foreach ($groups as $group_label => $fields) {
|
||||
$out .= '<div class="assoc-profile-group">';
|
||||
$out .= '<h5 class="assoc-group-label">' . assoc_h($group_label) . '</h5>';
|
||||
foreach ($fields as $field) {
|
||||
if (in_array($field, ['assoc_registered', 'assoc_updated'])) continue;
|
||||
$val = $entry[$field] ?? '';
|
||||
$label = $labels[$field] ?? $field;
|
||||
if (isset($allowed[$field])) {
|
||||
$out .= assoc_render_select($field, $label, $val, $allowed[$field]);
|
||||
} else {
|
||||
$out .= assoc_render_text($field, $label, $val);
|
||||
}
|
||||
}
|
||||
$out .= '</div>';
|
||||
}
|
||||
|
||||
$out .= '<div class="mt-3">';
|
||||
$out .= '<button type="submit" class="btn btn-primary btn-sm">Save Association Profile</button>';
|
||||
$out .= '</div>';
|
||||
$out .= '</form></div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
function assoc_render_text($field, $label, $value) {
|
||||
$id = assoc_h($field);
|
||||
return '<div class="assoc-field-wrap mb-2">
|
||||
<label class="form-label" for="' . $id . '">' . assoc_h($label) . '</label>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
id="' . $id . '" name="' . $id . '"
|
||||
value="' . assoc_h($value) . '">
|
||||
</div>';
|
||||
}
|
||||
|
||||
function assoc_render_select($field, $label, $value, $options) {
|
||||
$id = assoc_h($field);
|
||||
$out = '<div class="assoc-field-wrap mb-2">';
|
||||
$out .= '<label class="form-label" for="' . $id . '">' . assoc_h($label) . '</label>';
|
||||
$out .= '<select class="form-select form-select-sm" id="' . $id . '" name="' . $id . '">';
|
||||
foreach ($options as $opt) {
|
||||
$sel = ($value === $opt) ? 'selected' : '';
|
||||
$disp = ($opt === 'UNK') ? 'Unknown — not yet verified' : $opt;
|
||||
$out .= '<option value="' . assoc_h($opt) . '" ' . $sel . '>' . assoc_h($disp) . '</option>';
|
||||
}
|
||||
$out .= '</select></div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// SAVE HANDLER
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_profile_content() {
|
||||
if (argv(1) !== 'save') return '';
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return '';
|
||||
if (!local_channel()) return '';
|
||||
if (!assoc_verify_csrf()) {
|
||||
return '<div class="alert alert-danger">Invalid form token.</div>';
|
||||
}
|
||||
|
||||
$slug = substr(strip_tags($_POST['assoc_slug'] ?? ''), 0, 128);
|
||||
if (!$slug) return '<div class="alert alert-danger">Missing association slug.</div>';
|
||||
|
||||
$registry = assoc_load_registry();
|
||||
if (!isset($registry[$slug])) {
|
||||
return '<div class="alert alert-danger">Association not found in registry.</div>';
|
||||
}
|
||||
|
||||
// Verify caller is channel owner for this slug
|
||||
$channel = App::get_channel();
|
||||
if (assoc_slug_from_address($channel['channel_address'] ?? '') !== $slug) {
|
||||
return '<div class="alert alert-danger">Not authorized.</div>';
|
||||
}
|
||||
|
||||
$allowed = assoc_allowed_values();
|
||||
$fields = assoc_field_names();
|
||||
|
||||
foreach ($fields as $field) {
|
||||
if (in_array($field, ['assoc_registered', 'assoc_updated'])) continue;
|
||||
if (!isset($_POST[$field])) continue;
|
||||
$val = substr(strip_tags((string) $_POST[$field]), 0, 512);
|
||||
if (isset($allowed[$field]) && !assoc_validate_select($field, $val)) continue;
|
||||
$registry[$slug][$field] = $val;
|
||||
}
|
||||
|
||||
$registry[$slug]['assoc_updated'] = date('Y-m-d');
|
||||
|
||||
if (assoc_write_registry($registry)) {
|
||||
goaway(z_root() . '/profile/' . $slug);
|
||||
}
|
||||
|
||||
return '<div class="alert alert-danger">Failed to save. Check server logs.</div>';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// FIELD METADATA
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
function assoc_field_labels() {
|
||||
return [
|
||||
'assoc_legal_name' => 'Legal Name',
|
||||
'assoc_type' => 'Association Type',
|
||||
'assoc_statute' => 'Governing Statute',
|
||||
'assoc_street' => 'Street Address',
|
||||
'assoc_city' => 'City',
|
||||
'assoc_county' => 'County',
|
||||
'assoc_state' => 'State',
|
||||
'assoc_zip' => 'ZIP Code',
|
||||
'assoc_placekey' => 'Placekey',
|
||||
'assoc_website' => 'Association Website',
|
||||
'assoc_buildings' => 'Number of Buildings',
|
||||
'assoc_units' => 'Number of Units',
|
||||
'assoc_year_built' => 'Year Built',
|
||||
'assoc_stories' => 'Stories Per Building',
|
||||
'assoc_mgmt_company' => 'Management Company',
|
||||
'assoc_mgmt_contact' => 'Management Contact',
|
||||
'assoc_mgmt_certified' => 'Manager IDFPR Certified',
|
||||
'assoc_cai_listing' => 'CAI Directory Listing',
|
||||
'assoc_sos_id' => 'SOS Entity File Number',
|
||||
'assoc_sos_standing' => 'Corporate Standing',
|
||||
'assoc_declaration_recorded' => 'Declaration Recorded',
|
||||
'assoc_declaration_instrument' => 'Declaration Instrument No.',
|
||||
'assoc_registered' => 'Registered Date',
|
||||
'assoc_updated' => 'Last Updated',
|
||||
];
|
||||
}
|
||||
|
||||
function assoc_field_groups() {
|
||||
return [
|
||||
'Identity' => [
|
||||
'assoc_legal_name','assoc_type','assoc_statute',
|
||||
'assoc_street','assoc_city','assoc_county',
|
||||
'assoc_state','assoc_zip','assoc_placekey','assoc_website',
|
||||
],
|
||||
'Physical Structure' => [
|
||||
'assoc_buildings','assoc_units','assoc_year_built','assoc_stories',
|
||||
],
|
||||
'Governance and Management' => [
|
||||
'assoc_mgmt_company','assoc_mgmt_contact','assoc_mgmt_certified',
|
||||
'assoc_cai_listing',
|
||||
'assoc_sos_id','assoc_sos_standing',
|
||||
'assoc_declaration_recorded','assoc_declaration_instrument',
|
||||
],
|
||||
'Record' => [
|
||||
'assoc_registered','assoc_updated',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// CSRF
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -370,8 +434,7 @@ function assoc_csrf_token() {
|
||||
if (empty($_SESSION['assoc_profile_csrf'])) {
|
||||
$_SESSION['assoc_profile_csrf'] = bin2hex(random_bytes(16));
|
||||
}
|
||||
return '<input type="hidden" name="assoc_profile_csrf" value="'
|
||||
. assoc_h($_SESSION['assoc_profile_csrf']) . '">';
|
||||
return '<input type="hidden" name="assoc_profile_csrf" value="' . assoc_h($_SESSION['assoc_profile_csrf']) . '">';
|
||||
}
|
||||
|
||||
function assoc_verify_csrf() {
|
||||
|
||||
198
hubzilla/addon/assoc_profile/assoc_profile_manage.php
Normal file
198
hubzilla/addon/assoc_profile/assoc_profile_manage.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Association Profile — Management Renderer
|
||||
* Render functions for /assoc_profile/manage interface.
|
||||
* Operator-only. Called from assoc_profile.php content router.
|
||||
*/
|
||||
|
||||
function assoc_render_manage_index() {
|
||||
$registry = assoc_load_registry();
|
||||
$out = '<div class="assoc-manage">';
|
||||
$out .= '<h2>Association Profile — Management</h2>';
|
||||
$out .= '<div class="mb-3">';
|
||||
$out .= '<a href="' . z_root() . '/assoc_profile/manage/assoc" class="btn btn-primary btn-sm me-2">Add Association</a>';
|
||||
$out .= '<a href="' . z_root() . '/assoc_profile/manage/fields" class="btn btn-outline-secondary btn-sm">Manage Fields</a>';
|
||||
$out .= '</div>';
|
||||
if (empty($registry)) {
|
||||
$out .= '<p class="text-muted">No associations registered.</p>';
|
||||
} else {
|
||||
$out .= '<table class="table table-sm table-hover">';
|
||||
$out .= '<thead><tr><th>Slug</th><th>Legal Name</th><th>Type</th><th>Units</th><th>Updated</th><th></th></tr></thead><tbody>';
|
||||
foreach ($registry as $slug => $entry) {
|
||||
$out .= '<tr>';
|
||||
$out .= '<td><code>' . assoc_h($slug) . '</code></td>';
|
||||
$out .= '<td>' . assoc_h($entry['assoc_legal_name'] ?? '—') . '</td>';
|
||||
$out .= '<td>' . assoc_h($entry['assoc_type'] ?? '—') . '</td>';
|
||||
$out .= '<td>' . assoc_h($entry['assoc_units'] ?? '—') . '</td>';
|
||||
$out .= '<td>' . 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-primary">Edit</a></td>';
|
||||
$out .= '</tr>';
|
||||
}
|
||||
$out .= '</tbody></table>';
|
||||
}
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
function assoc_render_add_association_form() {
|
||||
$out = '<div class="assoc-manage">';
|
||||
$out .= '<h2>Add Association</h2>';
|
||||
$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">Slug <small class="text-muted">(lowercase, hyphens only — cannot be changed after creation)</small></label>';
|
||||
$out .= '<input type="text" class="form-control" name="new_slug" placeholder="e.g. arbors-bgca" required pattern="[a-z0-9\-]+">';
|
||||
$out .= '</div>';
|
||||
$out .= '<button type="submit" class="btn btn-primary">Create Association</button> ';
|
||||
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary">Cancel</a>';
|
||||
$out .= '</form></div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
function assoc_render_edit_association_form($slug) {
|
||||
$registry = assoc_load_registry();
|
||||
if (!isset($registry[$slug])) {
|
||||
return '<div class="alert alert-warning">Association not found: ' . assoc_h($slug) . '</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;
|
||||
}
|
||||
|
||||
$out = '<div class="assoc-manage">';
|
||||
$out .= '<h2>' . assoc_h($entry['assoc_legal_name'] ?? $slug) . '</h2>';
|
||||
$out .= '<p class="text-muted"><code>' . assoc_h($slug) . '</code></p>';
|
||||
$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) . '">';
|
||||
|
||||
foreach ($groups as $group) {
|
||||
$group_fields = $by_group[$group] ?? [];
|
||||
if (empty($group_fields)) continue;
|
||||
$out .= '<h5 class="assoc-group-label mt-4">' . assoc_h($group) . '</h5>';
|
||||
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-2">';
|
||||
$out .= '<label class="form-label text-muted">' . assoc_h($label) . '</label>';
|
||||
$out .= '<div class="form-control-plaintext ps-0">' . assoc_h($val ?: '—') . '</div>';
|
||||
$out .= '</div>';
|
||||
continue;
|
||||
}
|
||||
|
||||
$out .= '<div class="mb-2">';
|
||||
$out .= '<label class="form-label" for="' . assoc_h($nick) . '">' . assoc_h($label) . '</label>';
|
||||
if ($help) $out .= '<small class="text-muted d-block mb-1">' . assoc_h($help) . '</small>';
|
||||
|
||||
if ($type === 'select' && !empty($f['options'])) {
|
||||
$out .= '<select class="form-select form-select-sm" id="' . assoc_h($nick) . '" name="' . assoc_h($nick) . '">';
|
||||
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 {
|
||||
$out .= '<input type="text" class="form-control form-control-sm"
|
||||
id="' . assoc_h($nick) . '" name="' . assoc_h($nick) . '"
|
||||
value="' . assoc_h($val) . '">';
|
||||
}
|
||||
$out .= '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
$out .= '<div class="mt-4">';
|
||||
$out .= '<button type="submit" class="btn btn-primary">Save</button> ';
|
||||
$out .= '<a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary">Back</a>';
|
||||
$out .= '</div></form></div>';
|
||||
return $out;
|
||||
}
|
||||
|
||||
function assoc_render_fields_form() {
|
||||
$def = assoc_load_fields();
|
||||
$groups = $def['groups'] ?? [];
|
||||
$fields = $def['fields'] ?? [];
|
||||
|
||||
$out = '<div class="assoc-manage">';
|
||||
$out .= '<h2>Manage Fields</h2>';
|
||||
$out .= '<p class="text-muted small">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.</p>';
|
||||
|
||||
$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 .= '<table class="table table-sm">';
|
||||
$out .= '<thead><tr><th>Nickname</th><th>Label</th><th>Type</th><th>Group</th><th>Help</th><th></th></tr></thead><tbody>';
|
||||
foreach ($fields as $f) {
|
||||
$nick = $f['nickname'];
|
||||
$out .= '<tr>';
|
||||
$out .= '<td><code>' . 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'] ?? '') . '"></td>';
|
||||
$out .= '<td>' . assoc_h($f['type'] ?? 'text') . '</td>';
|
||||
$out .= '<td><input type="text" class="form-control form-control-sm" name="group_' . assoc_h($nick) . '" value="' . assoc_h($f['group'] ?? '') . '"></td>';
|
||||
$out .= '<td><input type="text" class="form-control form-control-sm" name="help_' . assoc_h($nick) . '" value="' . assoc_h($f['help'] ?? '') . '"></td>';
|
||||
$out .= '<td>';
|
||||
if (($f['type'] ?? '') !== 'readonly') {
|
||||
$out .= '<button type="submit" form="rm-' . assoc_h($nick) . '" class="btn btn-danger btn-sm"
|
||||
onclick="return confirm(\'Remove field ' . assoc_h($nick) . ' from ALL associations? Cannot be undone.\')">Remove</button>';
|
||||
}
|
||||
$out .= '</td></tr>';
|
||||
}
|
||||
$out .= '</tbody></table>';
|
||||
$out .= '<button type="submit" class="btn btn-primary btn-sm">Save Label/Group Changes</button>';
|
||||
$out .= '</form>';
|
||||
|
||||
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">';
|
||||
$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>';
|
||||
}
|
||||
|
||||
$out .= '<hr class="mt-4"><h5>Add New Field</h5>';
|
||||
$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-2">';
|
||||
$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"></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 <small>(select only, one per line)</small></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-2"><button type="submit" class="btn btn-success btn-sm">Add Field</button></div>';
|
||||
$out .= '</form>';
|
||||
|
||||
$out .= '<hr class="mt-4"><h5>Group Order</h5>';
|
||||
$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-2"><label class="form-label">Groups <small class="text-muted">(one per line)</small></label>';
|
||||
$out .= '<textarea class="form-control form-control-sm" name="groups" rows="' . count($groups) . '">' . assoc_h(implode("\n", $groups)) . '</textarea></div>';
|
||||
$out .= '<button type="submit" class="btn btn-primary btn-sm">Save Group Order</button>';
|
||||
$out .= '</form>';
|
||||
|
||||
$out .= '<div class="mt-3"><a href="' . z_root() . '/assoc_profile/manage" class="btn btn-outline-secondary btn-sm">Back</a></div>';
|
||||
$out .= '</div>';
|
||||
return $out;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"_note": "Copy to config.json. Do not commit config.json.",
|
||||
"registry_file": "REPLACE — absolute path to assoc_registry.json on the host, e.g. /var/www/hubzilla/addon/assoc_profile/assoc_registry.json"
|
||||
"registry_file": "REPLACE — absolute path to assoc_registry.json, e.g. /var/www/hubzilla/addon/assoc_profile/assoc_registry.json",
|
||||
"fields_file": "REPLACE — absolute path to assoc_fields.json, e.g. /var/www/hubzilla/addon/assoc_profile/assoc_fields.json"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user