Files
kane-diagnostics/hubzilla/addon/vs01/vs01_renderer.php
2026-06-21 09:38:35 -04:00

332 lines
14 KiB
PHP

<?php
/**
* VS-01 Renderer
* Reads VS JSON schemas and produces form HTML.
* This file knows nothing about what a Vital Sign is.
* It only knows how to render what the schema describes.
* See VS-RENDERER-SPEC.md for the full contract.
*/
// ----------------------------------------------------------------------------
// SCHEMA LOADER
// ----------------------------------------------------------------------------
function vs01_load_schemas() {
$dir = 'addon/vs01/vital-signs/';
$files = glob($dir . 'VS-*.json');
if (!$files) return [];
sort($files);
$schemas = [];
foreach ($files as $file) {
$raw = @file_get_contents($file);
if (!$raw) {
logger('vs01_renderer: could not read schema file: ' . $file);
continue;
}
$data = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE) {
logger('vs01_renderer: malformed schema file: ' . $file);
continue;
}
$code = $data['_meta']['code'] ?? null;
if ($code) $schemas[$code] = $data;
}
return $schemas;
}
// ----------------------------------------------------------------------------
// FIELD TYPE RENDERERS
// ----------------------------------------------------------------------------
function vs01_render_field_text($field, $value = '') {
$id = vs01_h($field['id']);
$out = '<div class="vs01-field vs01-field-text mb-3" data-field-id="' . $id . '">';
$out .= '<label class="form-label" for="' . $id . '">' . vs01_h($field['label']) . '</label>';
$out .= '<input type="text" class="form-control" id="' . $id . '"
name="fields[' . $id . ']"
value="' . vs01_h($value) . '"
placeholder="' . vs01_h($field['placeholder'] ?? '') . '"
' . (!empty($field['required']) ? 'required' : '') . '>';
$out .= '</div>';
return $out;
}
function vs01_render_field_number($field, $value = '') {
$id = vs01_h($field['id']);
$out = '<div class="vs01-field vs01-field-number mb-3" data-field-id="' . $id . '">';
$out .= '<label class="form-label" for="' . $id . '">' . vs01_h($field['label']) . '</label>';
$out .= '<input type="number" class="form-control" id="' . $id . '"
name="fields[' . $id . ']"
value="' . vs01_h($value) . '"
placeholder="' . vs01_h($field['placeholder'] ?? '') . '"
' . (!empty($field['required']) ? 'required' : '') . '>';
$out .= '</div>';
return $out;
}
function vs01_render_field_date($field, $value = '') {
$id = vs01_h($field['id']);
$out = '<div class="vs01-field vs01-field-date mb-3" data-field-id="' . $id . '">';
$out .= '<label class="form-label" for="' . $id . '">' . vs01_h($field['label']) . '</label>';
$out .= '<input type="date" class="form-control" id="' . $id . '"
name="fields[' . $id . ']"
value="' . vs01_h($value) . '"
' . (!empty($field['required']) ? 'required' : '') . '>';
$out .= '</div>';
return $out;
}
function vs01_render_field_boolean($field, $value = '') {
$id = vs01_h($field['id']);
$yes = ($value === 'true') ? 'checked' : '';
$no = ($value === 'false') ? 'checked' : '';
$req = !empty($field['required']) ? 'required' : '';
$out = '<div class="vs01-field vs01-field-boolean mb-3" data-field-id="' . $id . '">';
$out .= '<fieldset class="vs01-boolean-group">';
$out .= '<legend class="form-label">' . vs01_h($field['label']) . '</legend>';
$out .= '<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="' . $id . '_yes"
name="fields[' . $id . ']" value="true" ' . $yes . ' ' . $req . '>
<label class="form-check-label" for="' . $id . '_yes">Yes</label>
</div>';
$out .= '<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" id="' . $id . '_no"
name="fields[' . $id . ']" value="false" ' . $no . '>
<label class="form-check-label" for="' . $id . '_no">No</label>
</div>';
$out .= '</fieldset></div>';
return $out;
}
function vs01_render_field_select($field, $value = '') {
$id = vs01_h($field['id']);
$options = $field['options'] ?? [];
$out = '<div class="vs01-field vs01-field-select mb-3" data-field-id="' . $id . '">';
$out .= '<label class="form-label" for="' . $id . '">' . vs01_h($field['label']) . '</label>';
$out .= '<select class="form-select" id="' . $id . '" name="fields[' . $id . ']"
' . (!empty($field['required']) ? 'required' : '') . '>';
$out .= '<option value="">— select —</option>';
foreach ($options as $opt) {
$v = vs01_h($opt['value']);
$l = vs01_h($opt['label']);
$sel = ($value === $opt['value']) ? 'selected' : '';
$out .= '<option value="' . $v . '" ' . $sel . '>' . $l . '</option>';
}
$out .= '</select></div>';
return $out;
}
function vs01_render_field_textarea($field, $value = '') {
$id = vs01_h($field['id']);
$out = '<div class="vs01-field vs01-field-textarea mb-3" data-field-id="' . $id . '">';
$out .= '<label class="form-label" for="' . $id . '">' . vs01_h($field['label']) . '</label>';
$out .= '<textarea class="form-control" id="' . $id . '"
name="fields[' . $id . ']"
placeholder="' . vs01_h($field['placeholder'] ?? '') . '"
rows="5"
' . (!empty($field['required']) ? 'required' : '') . '>'
. vs01_h($value) . '</textarea>';
$out .= '</div>';
return $out;
}
function vs01_render_field_multiselect($field, $value = '') {
$id = vs01_h($field['id']);
$options = $field['options'] ?? [];
$selected = $value ? json_decode($value, true) : [];
if (!is_array($selected)) $selected = [];
$out = '<div class="vs01-field vs01-field-multiselect mb-3" data-field-id="' . $id . '">';
$out .= '<fieldset><legend class="form-label">' . vs01_h($field['label']) . '</legend>';
foreach ($options as $opt) {
$v = vs01_h($opt['value']);
$l = vs01_h($opt['label']);
$chk = in_array($opt['value'], $selected) ? 'checked' : '';
$out .= '<div class="form-check">
<input class="form-check-input" type="checkbox"
id="' . $id . '_' . $v . '"
name="fields[' . $id . '][]"
value="' . $v . '" ' . $chk . '>
<label class="form-check-label" for="' . $id . '_' . $v . '">' . $l . '</label>
</div>';
}
$out .= '</fieldset></div>';
return $out;
}
// ----------------------------------------------------------------------------
// FIELD DISPATCHER
// ----------------------------------------------------------------------------
function vs01_render_field($field, $submitted_values = []) {
$id = $field['id'] ?? '';
$type = $field['type'] ?? 'text';
// A schema field may declare a "default" (e.g. ho_total_units => 0) so the
// input isn't left blank on first render. Submitted values always win;
// this only applies on the initial, un-submitted render of the form.
$value = $submitted_values[$id] ?? (isset($field['default']) ? (string) $field['default'] : '');
switch ($type) {
case 'text': $html = vs01_render_field_text($field, $value); break;
case 'number': $html = vs01_render_field_number($field, $value); break;
case 'date': $html = vs01_render_field_date($field, $value); break;
case 'boolean': $html = vs01_render_field_boolean($field, $value); break;
case 'select': $html = vs01_render_field_select($field, $value); break;
case 'textarea': $html = vs01_render_field_textarea($field, $value); break;
case 'multiselect': $html = vs01_render_field_multiselect($field, $value); break;
default:
$html = '<div class="alert alert-danger">Unknown field type: ' . vs01_h($type) . '</div>';
}
$html .= vs01_render_diagnostic_flag($field, $value);
return $html;
}
// ----------------------------------------------------------------------------
// CONDITIONAL DISPLAY
// ----------------------------------------------------------------------------
function vs01_wrap_depends_on($field, $html) {
if (empty($field['depends_on'])) return $html;
$dep_field = vs01_h($field['depends_on']['field']);
$dep_value = $field['depends_on']['value'];
$dep_value = is_array($dep_value)
? vs01_h(json_encode($dep_value))
: vs01_h((string) $dep_value);
return '<div class="vs01-conditional"
data-depends-on="' . $dep_field . '"
data-depends-value="' . $dep_value . '"
style="display:none;">'
. $html .
'</div>';
}
// ----------------------------------------------------------------------------
// DIAGNOSTIC FLAGS
// ----------------------------------------------------------------------------
function vs01_render_diagnostic_flag($field, $value) {
if (empty($field['diagnostic_flag'])) return '';
$condition = $field['flag_condition'] ?? null;
if ($condition === null) return '';
$triggered = false;
if (is_array($condition)) {
$triggered = in_array($value, $condition);
} elseif (is_bool($condition)) {
$triggered = ($value === ($condition ? 'true' : 'false'));
} else {
$triggered = ($value === (string) $condition);
}
if (!$triggered) return '';
$severity = vs01_flag_severity($field);
$label = vs01_h($field['flag_label'] ?? 'Diagnostic condition');
$note = isset($field['flag_note'])
? '<p class="vs01-flag-note mb-0 mt-1 small">' . vs01_h($field['flag_note']) . '</p>'
: '';
return '<div class="vs01-flag vs01-flag-' . $severity . ' mb-2" role="alert">
<strong>' . $label . '</strong>' . $note . '
</div>';
}
function vs01_flag_severity($field) {
$id = $field['id'] ?? '';
if (in_array($id, ['pro_selfdealing_verified', 'pro_standing_verified', 'pro_incumbent_perpetuation_risk'])) {
return 'critical';
}
if (str_starts_with($id, 'pro_')) return 'high';
return 'standard';
}
// ----------------------------------------------------------------------------
// COMPOUND CONDITION BANNERS
// ----------------------------------------------------------------------------
function vs01_render_compound_banners($association_slug) {
// TODO: query stored VS records for this association from orchestrator
// and evaluate the three compound conditions defined in VS-RENDERER-SPEC.md.
// Returns empty string until orchestrator query is implemented.
return '';
}
// ----------------------------------------------------------------------------
// CROSS-VS REFERENCE NOTES
// ----------------------------------------------------------------------------
function vs01_render_cross_reference($vs_code, $label, $association_slug) {
$url = z_root() . '/vs01/' . vs01_h($association_slug) . '/' . vs01_h($vs_code);
return '<div class="vs01-cross-ref small text-muted mt-1">
See also: <a href="' . $url . '">' . vs01_h($label) . '</a>
</div>';
}
// ----------------------------------------------------------------------------
// VERIFICATION SOURCES PANEL
// ----------------------------------------------------------------------------
function vs01_render_verification_sources($meta) {
$sources = $meta['verification_sources'] ?? [];
if (empty($sources)) return '';
$code = vs01_h($meta['code'] ?? 'vs');
$out = '<div class="vs01-verification-sources mb-3">';
$out .= '<button type="button" class="btn btn-sm btn-outline-secondary"
data-bs-toggle="collapse"
data-bs-target="#vs01-sources-' . $code . '"
aria-expanded="false">
Where to verify this
</button>';
$out .= '<div class="collapse mt-2" id="vs01-sources-' . $code . '">';
foreach ($sources as $source) {
$label = vs01_h($source['label'] ?? '');
$url = vs01_h($source['url'] ?? '');
$instruction = vs01_h($source['instruction'] ?? '');
$out .= '<div class="vs01-source mb-2">';
$out .= '<a href="' . $url . '" target="_blank" rel="noopener" class="fw-semibold">' . $label . '</a>';
$out .= '<p class="small text-muted mb-0">' . $instruction . '</p>';
$out .= '</div>';
}
$out .= '</div></div>';
return $out;
}
// ----------------------------------------------------------------------------
// PERSPECTIVE NOTE (VS-07 and VS-09 homeowner slot)
// ----------------------------------------------------------------------------
function vs01_render_perspective_note($schema, $perspective_key) {
if ($perspective_key !== 'homeowner') return '';
$code = $schema['_meta']['code'] ?? '';
if (!in_array($code, ['VS-07', 'VS-09'])) return '';
$notes = $schema['_meta']['renderer_notes'] ?? '';
if (!$notes) return '';
return '<div class="vs01-perspective-note alert alert-light small mb-3">'
. vs01_h($notes) . '</div>';
}
// ----------------------------------------------------------------------------
// IMMUTABLE PUBLIC RECORD
// ----------------------------------------------------------------------------
function vs01_public_record_exists($vs_code, $association_slug) {
// TODO: query orchestrator for existing promoted public record
return false;
}
function vs01_render_public_record_readonly($perspective, $values) {
$out = '<div class="vs01-readonly">';
foreach ($perspective['fields'] ?? [] as $field) {
$id = vs01_h($field['id'] ?? '');
$label = vs01_h($field['label'] ?? '');
$value = vs01_h($values[$field['id'] ?? ''] ?? '—');
$out .= '<div class="vs01-readonly-field mb-2">';
$out .= '<span class="fw-semibold">' . $label . '</span> ';
$out .= '<span class="text-muted">' . $value . '</span>';
$out .= '</div>';
}
$out .= '</div>';
return $out;
}