<?php

declare(strict_types=1);

use PpPhp\Core\Events\EventApiReference;
use PpPhp\Core\Events\EventCatalogue;

/*
 * Events handbook.
 *
 * The event and API references below are deliberately generated from the same
 * catalogues used by the editor. New events, arguments, return contracts and
 * IntelliSense helpers therefore appear here without a second documentation
 * pass. The curated cookbook is the task-oriented layer above that reference.
 */
$evhGlobalGroups = EventCatalogue::enrich(EventCatalogue::globalGroups());
$evhTableGroups  = EventCatalogue::enrich(EventCatalogue::tableGroups());
$evhFieldGroups  = EventCatalogue::enrich(EventCatalogue::fieldGroups());
$evhPhpApi       = EventApiReference::php();
$evhJsApi        = EventApiReference::js();

$evhH = static fn (mixed $value): string => htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
$evhEventCount = static function (array $groups): int {
    return array_sum(array_map(static fn (array $group): int => count($group['events'] ?? []), $groups));
};
$evhGroupApi = static function (array $rows): array {
    $grouped = [];
    foreach ($rows as $row) {
        $grouped[(string) ($row['category'] ?? 'Other')][] = $row;
    }
    return $grouped;
};
$evhPhpByCategory = $evhGroupApi($evhPhpApi);
$evhJsByCategory = $evhGroupApi($evhJsApi);
$evhTotalEvents = $evhEventCount($evhGlobalGroups) + $evhEventCount($evhTableGroups) + $evhEventCount($evhFieldGroups);

$evhRecipes = [
    [
        'group' => 'Forms & interface',
        'title' => 'Reveal contact fields after Full Name is entered',
        'event' => 'Table → Client → On Page Init',
        'lang' => 'js',
        'why' => 'Hide fields on first paint, then reveal them immediately as the user types. Replace the field names with the exact schema names suggested by IntelliSense.',
        'code' => <<<'JS'
const fullname = Runner.getControl('fullname');
const mobile = Runner.getControl('mobile');
const email = Runner.getControl('email');

function updateContactVisibility() {
    const hasName = String(fullname.getValue() || '').trim().length > 0;
    mobile.visible(hasName);
    email.visible(hasName);
}

updateContactVisibility();
fullname.onChange(updateContactVisibility);
fullname.on('keyup', updateContactVisibility);
JS,
    ],
    [
        'group' => 'Forms & interface',
        'title' => 'Show VAT number only for company customers',
        'event' => 'Table → Client → On Page Init',
        'lang' => 'js',
        'why' => 'A reusable conditional-field pattern. The same function handles the initial state and later changes.',
        'code' => <<<'JS'
const type = Runner.getControl('customer_type');
const vat = Runner.getControl('vat_number');

function updateVatField() {
    const isCompany = Runner.getControlValue('customer_type') === 'company';
    vat.visible(isCompany);
    vat.required(isCompany);
    if (!isCompany) Runner.setControlValue('vat_number', '');
}

updateVatField();
type.onChange(updateVatField);
JS,
    ],
    [
        'group' => 'Forms & interface',
        'title' => 'Calculate a line total while the user types',
        'event' => 'Table → Client → On Page Init',
        'lang' => 'js',
        'why' => 'Runner.compute watches dependencies and writes the derived value without a reload.',
        'code' => <<<'JS'
Runner.compute('line_total', ['quantity', 'unit_price'], function (qty, price) {
    return ((Number(qty) || 0) * (Number(price) || 0)).toFixed(2);
});
JS,
    ],
    [
        'group' => 'Forms & interface',
        'title' => 'Build a URL slug from a name',
        'event' => 'Table → Client → On Page Init',
        'lang' => 'js',
        'why' => 'Runner.link keeps a target field synchronized through a transform.',
        'code' => <<<'JS'
Runner.link('name', 'slug', function (value) {
    return String(value || '')
        .trim()
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-|-$/g, '');
});
JS,
    ],
    [
        'group' => 'Forms & interface',
        'title' => 'Disable a field unless the user is an administrator',
        'event' => 'Table → Client → On Page Init',
        'lang' => 'js',
        'why' => 'Useful as a UI convenience; enforce the same rule again on the server because browser rules are not security boundaries.',
        'code' => <<<'JS'
// In a server event, expose this value with:
// pp_set_proxy('mayApprove', Security::CurrentUserGroup() === 'admin');
const mayApprove = Boolean(pp.getProxy('mayApprove', false));
Runner.getControl('approval_status').readonly(!mayApprove);
JS,
    ],
    [
        'group' => 'Forms & interface',
        'title' => 'Set safe defaults before Add opens',
        'event' => 'Table → Page → Before Add Load',
        'lang' => 'php',
        'why' => 'Defaults are changed by reference before controls render.',
        'code' => <<<'PHP'
<?php
$defaults['status'] = 'draft';
$defaults['created_at'] = date('Y-m-d H:i:s');
$defaults['created_by'] = (int) (Security::CurrentUserData()['id'] ?? 0);
PHP,
    ],
    [
        'group' => 'Forms & interface',
        'title' => 'Stop a form submission in the browser',
        'event' => 'Table → Client → Before Form Submit',
        'lang' => 'js',
        'why' => 'Gives immediate feedback. Repeat important validation in Before Add Save or Before Edit Save.',
        'code' => <<<'JS'
if (!data.accept_terms) {
    pp.alert('Please accept the terms before continuing.', 'Terms required');
    Runner.getControl('accept_terms').focus();
    return false;
}
JS,
    ],
    [
        'group' => 'Validation & data',
        'title' => 'Require a unique email address',
        'event' => 'Table → Page → Before Add Save',
        'lang' => 'php',
        'why' => 'Prepared parameters prevent injection. A field-keyed error appears beside the matching control.',
        'code' => <<<'PHP'
<?php
$email = strtolower(trim((string) ($data['email'] ?? '')));
$data['email'] = $email;

if (dbCount('SELECT COUNT(*) FROM users WHERE email = ?', [$email]) > 0) {
    return ['email' => 'This email address is already registered.'];
}
PHP,
    ],
    [
        'group' => 'Validation & data',
        'title' => 'Protect ownership during Edit',
        'event' => 'Table → Page → Before Edit Save',
        'lang' => 'php',
        'why' => 'Never trust a hidden or read-only browser control for authorization-sensitive values.',
        'code' => <<<'PHP'
<?php
$user = Security::CurrentUserData();
$isAdmin = ($user['role'] ?? '') === 'admin';

if (!$isAdmin) {
    $data['created_by'] = $old['created_by'] ?? null;
}
PHP,
    ],
    [
        'group' => 'Validation & data',
        'title' => 'Normalize a phone number before it is stored',
        'event' => 'Field → Server → Before Set',
        'lang' => 'php',
        'why' => 'Field scope keeps one normalization rule consistent across Add, Edit, inline edit, import, and API writes.',
        'code' => <<<'PHP'
<?php
$digits = preg_replace('/\D+/', '', (string) $value);
$value = $digits === '' ? null : '+' . ltrim($digits, '+');
PHP,
    ],
    [
        'group' => 'Validation & data',
        'title' => 'Validate a date range',
        'event' => 'Field → Server → Validate',
        'lang' => 'php',
        'why' => 'Return an error string to reject the field, or null when valid.',
        'code' => <<<'PHP'
<?php
$start = strtotime((string) ($record['start_date'] ?? ''));
$end = strtotime((string) $value);

if ($start && $end && $end < $start) {
    return 'End date must be on or after the start date.';
}
return null;
PHP,
    ],
    [
        'group' => 'Validation & data',
        'title' => 'Calculate a server-trusted total',
        'event' => 'Field → Server → Calculate',
        'lang' => 'php',
        'why' => 'Use a server calculation even when the UI also calculates live; the browser value can be manipulated.',
        'code' => <<<'PHP'
<?php
$quantity = (float) ($record['quantity'] ?? 0);
$price = (float) ($record['unit_price'] ?? 0);
return round($quantity * $price, 2);
PHP,
    ],
    [
        'group' => 'Validation & data',
        'title' => 'Format a status as a badge',
        'event' => 'Field → Server → Format',
        'lang' => 'php',
        'why' => 'Formatting changes presentation, not the stored value.',
        'code' => <<<'PHP'
<?php
$status = strtolower((string) $value);
$label = htmlspecialchars(ucfirst($status), ENT_QUOTES, 'UTF-8');
$class = in_array($status, ['active', 'approved'], true) ? 'success' : 'secondary';
return '<span class="badge bg-' . $class . '">' . $label . '</span>';
PHP,
    ],
    [
        'group' => 'Lists, search & access',
        'title' => 'Enforce tenant isolation in every list query',
        'event' => 'Table → SQL → SQL Where',
        'lang' => 'php',
        'why' => 'Return only the condition, without the WHERE keyword. Values placed directly into SQL must be trusted integers or safely quoted.',
        'code' => <<<'PHP'
<?php
$tenantId = (int) ($user['tenant_id'] ?? 0);
if ($tenantId <= 0) return '1 = 0';
return 'tenant_id = ' . $tenantId;
PHP,
    ],
    [
        'group' => 'Lists, search & access',
        'title' => 'Show users only their own records',
        'event' => 'Table → SQL → SQL Where',
        'lang' => 'php',
        'why' => 'Administrators see everything; other users are restricted to their owner id.',
        'code' => <<<'PHP'
<?php
if (($user['role'] ?? '') === 'admin') return '';
return 'created_by = ' . (int) ($user['id'] ?? 0);
PHP,
    ],
    [
        'group' => 'Lists, search & access',
        'title' => 'Apply a business-friendly default order',
        'event' => 'Table → SQL → SQL Order By',
        'lang' => 'php',
        'why' => 'Return the ordering expression without ORDER BY.',
        'code' => <<<'PHP'
<?php
return 'priority DESC, due_date ASC, id DESC';
PHP,
    ],
    [
        'group' => 'Lists, search & access',
        'title' => 'Highlight overdue rows after the list renders',
        'event' => 'Table → Client → After List Render',
        'lang' => 'js',
        'why' => 'Use the rendered records and rows without changing stored data.',
        'code' => <<<'JS'
data.forEach(function (record) {
    if (record.due_date && new Date(record.due_date) < new Date() && record.status !== 'done') {
        const row = container.querySelector('[data-id="' + record.id + '"]');
        if (row) row.classList.add('table-warning');
    }
});
JS,
    ],
    [
        'group' => 'Lists, search & access',
        'title' => 'Open a custom record view on row click',
        'event' => 'Table → Client → On Row Click',
        'lang' => 'js',
        'why' => 'Return false to suppress the normal row behavior after performing your own action.',
        'code' => <<<'JS'
if (event.target.closest('button, a, input')) return;
window.location.href = 'view.php?id=' + encodeURIComponent(record.id);
return false;
JS,
    ],
    [
        'group' => 'Lists, search & access',
        'title' => 'React to multiple selected rows',
        'event' => 'Table → Client → On Row Select',
        'lang' => 'js',
        'why' => 'Enable a bulk action only when the current selection makes sense.',
        'code' => <<<'JS'
const button = Runner.button('approveSelected');
button.visible(ids.length > 0);
if (records.every(record => record.status === 'pending')) {
    button.enable();
} else {
    button.disable();
}
JS,
    ],
    [
        'group' => 'Record workflows',
        'title' => 'Create a related profile after registration',
        'event' => 'Global → Authentication → After Register',
        'lang' => 'php',
        'why' => 'The user row already exists, so related records can safely use its id.',
        'code' => <<<'PHP'
<?php
dbExec(
    'INSERT INTO profiles (user_id, display_name, created_at) VALUES (?, ?, ?)',
    [$user['id'], $user['fullname'] ?? $user['username'], date('Y-m-d H:i:s')]
);
PHP,
    ],
    [
        'group' => 'Record workflows',
        'title' => 'Create initial child rows after an order is added',
        'event' => 'Table → Page → After Add Save',
        'lang' => 'php',
        'why' => 'Use the generated id only in an After Add event.',
        'code' => <<<'PHP'
<?php
dbExec(
    'INSERT INTO order_status_history (order_id, status, changed_at) VALUES (?, ?, ?)',
    [$id, $record['status'] ?? 'new', date('Y-m-d H:i:s')]
);
PHP,
    ],
    [
        'group' => 'Record workflows',
        'title' => 'Write an audit diff after Edit',
        'event' => 'Table → Page → After Edit Save',
        'lang' => 'php',
        'why' => 'The event exposes both the saved record and its previous values.',
        'code' => <<<'PHP'
<?php
$changes = array_diff_assoc($record, $old);
if ($changes) {
    dbExec(
        'INSERT INTO audit_log (table_name, record_id, changes_json, user_id, created_at) VALUES (?, ?, ?, ?, ?)',
        ['orders', $id, json_encode($changes), (int) (Security::CurrentUserData()['id'] ?? 0), date('Y-m-d H:i:s')]
    );
}
PHP,
    ],
    [
        'group' => 'Record workflows',
        'title' => 'Prevent deletion while child records exist',
        'event' => 'Table → Page → Before Delete',
        'lang' => 'php',
        'why' => 'Cancel before destructive work begins.',
        'code' => <<<'PHP'
<?php
$children = dbCount('SELECT COUNT(*) FROM order_items WHERE order_id = ?', [$id]);
if ($children > 0) return false;
PHP,
    ],
    [
        'group' => 'Record workflows',
        'title' => 'Notify administrators after a new order',
        'event' => 'Table → Page → After Add Save',
        'lang' => 'php',
        'why' => 'Use the generated notification service for durable in-app alerts.',
        'code' => <<<'PHP'
<?php
\PpPhp\Core\Notification::AddForRole('admin', [
    'title' => 'New order',
    'message' => 'Order #' . $id . ' was created.',
    'type' => 'success',
    'url' => '/orders/view.php?id=' . $id,
]);
PHP,
    ],
    [
        'group' => 'Authentication & application',
        'title' => 'Redirect users after login by role',
        'event' => 'Global → Authentication → After Login',
        'lang' => 'php',
        'why' => 'Return a URL to override the default post-login destination.',
        'code' => <<<'PHP'
<?php
return ($user['role'] ?? '') === 'admin'
    ? '/admin/dashboard.php'
    : '/account/home.php';
PHP,
    ],
    [
        'group' => 'Authentication & application',
        'title' => 'Record failed login attempts',
        'event' => 'Global → Authentication → On Login Failed',
        'lang' => 'php',
        'why' => 'Treat the typed username as untrusted and log only what support/security needs.',
        'code' => <<<'PHP'
<?php
dbExec(
    'INSERT INTO login_failures (username, reason, ip_address, attempted_at) VALUES (?, ?, ?, ?)',
    [$username, $reason, (string) ($_SERVER['REMOTE_ADDR'] ?? ''), date('Y-m-d H:i:s')]
);
PHP,
    ],
    [
        'group' => 'Authentication & application',
        'title' => 'Hide administrator menu items',
        'event' => 'Global → Application → Menu Modify',
        'lang' => 'php',
        'why' => 'Modify the menu array by reference. This improves navigation; permissions must still be enforced on the destination.',
        'code' => <<<'PHP'
<?php
if (($user['role'] ?? '') !== 'admin') {
    $items = array_values(array_filter(
        $items,
        static fn (array $item): bool => ($item['id'] ?? '') !== 'admin'
    ));
}
PHP,
    ],
    [
        'group' => 'Authentication & application',
        'title' => 'Guard a page before processing',
        'event' => 'Global → Application → Before Process',
        'lang' => 'php',
        'why' => 'Return a URL to redirect or false to stop processing.',
        'code' => <<<'PHP'
<?php
if ($page === 'billing' && !in_array($user['role'] ?? '', ['admin', 'finance'], true)) {
    return '/access-denied.php';
}
PHP,
    ],
    [
        'group' => 'Email, files & integrations',
        'title' => 'Add a standard footer before email is sent',
        'event' => 'Global → Email → Before Send Email',
        'lang' => 'php',
        'why' => 'The subject and body can be changed by reference; return false to cancel.',
        'code' => <<<'PHP'
<?php
$body .= '<hr><p style="color:#64748b">Sent securely by Example App.</p>';
$data['headers']['X-App-Message'] = 'transactional';
PHP,
    ],
    [
        'group' => 'Email, files & integrations',
        'title' => 'Reject oversized or unsafe uploads',
        'event' => 'Global → File & Storage → Before File Upload',
        'lang' => 'php',
        'why' => 'Validate MIME, extension and size on the server; browser validation is only a convenience.',
        'code' => <<<'PHP'
<?php
$allowed = ['image/jpeg', 'image/png', 'application/pdf'];
if (($file['size'] ?? 0) > 5 * 1024 * 1024) return false;
if (!in_array($file['type'] ?? '', $allowed, true)) return false;

$file['name'] = bin2hex(random_bytes(12)) . '.' . strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
PHP,
    ],
    [
        'group' => 'Email, files & integrations',
        'title' => 'Protect a download by record ownership',
        'event' => 'Global → File & Storage → Before File Download',
        'lang' => 'php',
        'why' => 'Return false for a 403 response before file bytes are exposed.',
        'code' => <<<'PHP'
<?php
$ownerId = (int) dbValue('SELECT owner_id FROM documents WHERE stored_name = ?', [$file['name']], 0);
$user = Security::CurrentUserData();
if ($ownerId !== (int) ($user['id'] ?? 0) && ($user['role'] ?? '') !== 'admin') {
    return false;
}
PHP,
    ],
    [
        'group' => 'Email, files & integrations',
        'title' => 'Require an API key before an endpoint runs',
        'event' => 'Global → API & Webhooks → Before API Request',
        'lang' => 'php',
        'why' => 'Return false to reject the request with 403.',
        'code' => <<<'PHP'
<?php
$provided = (string) ($request['headers']['X-Api-Key'] ?? '');
$expected = (string) getenv('APP_API_KEY');
if ($expected === '' || !hash_equals($expected, $provided)) return false;
PHP,
    ],
    [
        'group' => 'Email, files & integrations',
        'title' => 'Return a consistent API envelope',
        'event' => 'Global → API & Webhooks → Before API Response',
        'lang' => 'php',
        'why' => 'Body and status can be changed by reference just before serialization.',
        'code' => <<<'PHP'
<?php
$body = [
    'ok' => $status >= 200 && $status < 400,
    'data' => $body,
    'meta' => ['endpoint' => $endpoint, 'generated_at' => gmdate(DATE_ATOM)],
];
PHP,
    ],
    [
        'group' => 'Email, files & integrations',
        'title' => 'Receive a signed webhook safely',
        'event' => 'Global → API & Webhooks → On Webhook Receive',
        'lang' => 'php',
        'why' => 'Verify the raw body before trusting parsed payload data. Return an array as the webhook response.',
        'code' => <<<'PHP'
<?php
$signature = (string) ($headers['X-Signature'] ?? '');
$expected = hash_hmac('sha256', $raw, (string) getenv('WEBHOOK_SECRET'));
if (!hash_equals($expected, $signature)) return false;

$eventId = (string) ($payload['id'] ?? '');
if ($eventId === '') return ['ok' => false, 'error' => 'Missing event id'];
if (dbCount('SELECT COUNT(*) FROM webhook_log WHERE event_id = ?', [$eventId])) {
    return ['ok' => true, 'duplicate' => true];
}

dbExec('INSERT INTO webhook_log (event_id, source, received_at) VALUES (?, ?, ?)', [
    $eventId, $source, date('Y-m-d H:i:s')
]);
return ['ok' => true];
PHP,
    ],
    [
        'group' => 'Real-time & browser bridge',
        'title' => 'Authorize a private real-time channel',
        'event' => 'Global → Real-time → On Channel Auth',
        'lang' => 'php',
        'why' => 'Reject channel subscriptions that do not match the current user or role.',
        'code' => <<<'PHP'
<?php
if (($user['role'] ?? '') === 'admin') return;
$expected = 'user.' . (int) ($user['id'] ?? 0);
if ($channel !== $expected) return false;
PHP,
    ],
    [
        'group' => 'Real-time & browser bridge',
        'title' => 'Sanitize data before broadcast',
        'event' => 'Global → Real-time → Before Broadcast',
        'lang' => 'php',
        'why' => 'Remove private keys from the payload by reference before it reaches clients.',
        'code' => <<<'PHP'
<?php
unset($payload['password'], $payload['token'], $payload['internal_notes']);
if (str_starts_with($channel, 'private.')
    && empty(Security::CurrentUserData()['id'])) return false;
PHP,
    ],
    [
        'group' => 'Import, export & print',
        'title' => 'Restrict exports by role',
        'event' => 'Table → Page → Before Export',
        'lang' => 'php',
        'why' => 'Return false before a file is generated.',
        'code' => <<<'PHP'
<?php
$user = Security::CurrentUserData();
if (!in_array($user['role'] ?? '', ['admin', 'analyst'], true)) return false;
PHP,
    ],
    [
        'group' => 'Import, export & print',
        'title' => 'Validate an import before processing',
        'event' => 'Table → Page → Before Import',
        'lang' => 'php',
        'why' => 'Inspect the uploaded file and mapping before database writes begin.',
        'code' => <<<'PHP'
<?php
$extension = strtolower(pathinfo((string) ($file['name'] ?? ''), PATHINFO_EXTENSION));
if (!in_array($extension, ['csv', 'xlsx'], true)) return false;
if (!isset($map['email'])) return false;
PHP,
    ],
    [
        'group' => 'Import, export & print',
        'title' => 'Notify an administrator after import',
        'event' => 'Table → Page → After Import',
        'lang' => 'php',
        'why' => 'Summarize the exact outcome counts after processing.',
        'code' => <<<'PHP'
<?php
\PpPhp\Core\Notification::AddForRole('admin', [
    'title' => 'Import completed',
    'message' => "Imported {$imported}, updated {$updated}, skipped {$skipped}, failed {$failed}.",
    'type' => $failed > 0 ? 'warning' : 'success',
]);
PHP,
    ],
    [
        'group' => 'Import, export & print',
        'title' => 'Add a print watermark',
        'event' => 'Table → Page → On Print',
        'lang' => 'php',
        'why' => 'Modify records by reference for print-only presentation.',
        'code' => <<<'PHP'
<?php
foreach ($records as &$row) {
    $row['_watermark'] = 'Printed ' . date('Y-m-d H:i') . ' by '
        . (Security::CurrentUsername() ?: 'system');
}
unset($row);
PHP,
    ],
    [
        'group' => 'Data validation & defaults',
        'title' => 'Require either email or mobile',
        'event' => 'Table → Page → Before Add Save',
        'lang' => 'php',
        'why' => 'Return field-specific messages when a record needs at least one contact method.',
        'code' => <<<'PHP'
<?php
$email = trim((string) ($data['email'] ?? ''));
$mobile = trim((string) ($data['mobile'] ?? ''));
if ($email === '' && $mobile === '') {
    return [
        'email' => 'Enter an email address or mobile number.',
        'mobile' => 'Enter a mobile number or email address.',
    ];
}
PHP,
    ],
    [
        'group' => 'Data validation & defaults',
        'title' => 'Stamp the signed-in creator safely',
        'event' => 'Table → Page → Before Add Save',
        'lang' => 'php',
        'why' => 'Set ownership on the server; never trust a browser-supplied owner id.',
        'code' => <<<'PHP'
<?php
$user = Security::CurrentUserData();
$data['created_by'] = (int) ($user['id'] ?? 0);
$data['created_at'] = date('Y-m-d H:i:s');
unset($data['updated_by']);
PHP,
    ],
    [
        'group' => 'Data validation & defaults',
        'title' => 'Allow only valid status transitions',
        'event' => 'Table → Page → Before Edit Save',
        'lang' => 'php',
        'why' => 'Protect workflow rules even when a request bypasses the visual controls.',
        'code' => <<<'PHP'
<?php
$from = (string) ($old['status'] ?? 'draft');
$to = (string) ($data['status'] ?? $from);
$allowed = [
    'draft' => ['pending', 'cancelled'],
    'pending' => ['approved', 'rejected', 'cancelled'],
    'approved' => ['completed'],
];
if ($to !== $from && !in_array($to, $allowed[$from] ?? [], true)) {
    return ['status' => "Status cannot change from {$from} to {$to}."];
}
PHP,
    ],
    [
        'group' => 'Data validation & defaults',
        'title' => 'Normalize optional values before saving',
        'event' => 'Table → Page → Before Add Save',
        'lang' => 'php',
        'why' => 'Store consistent nulls and canonical values instead of whitespace-only strings.',
        'code' => <<<'PHP'
<?php
foreach (['email', 'mobile', 'reference'] as $field) {
    $value = trim((string) ($data[$field] ?? ''));
    $data[$field] = $value === '' ? null : $value;
}
if (!empty($data['email'])) $data['email'] = strtolower($data['email']);
PHP,
    ],
    [
        'group' => 'Authorization & privacy',
        'title' => 'Redact private fields for non-managers',
        'event' => 'Table → Page → Before View Load',
        'lang' => 'php',
        'why' => 'Remove sensitive values on the server before the record reaches the page.',
        'code' => <<<'PHP'
<?php
$user = Security::CurrentUserData();
if (!in_array($user['role'] ?? '', ['admin', 'manager'], true)) {
    unset($record['salary'], $record['national_id'], $record['internal_notes']);
}
PHP,
    ],
    [
        'group' => 'Authorization & privacy',
        'title' => 'Prevent deleting your own account',
        'event' => 'Table → Page → Before Delete',
        'lang' => 'php',
        'why' => 'Avoid accidental administrator lockout from the users table.',
        'code' => <<<'PHP'
<?php
$me = (int) (Security::CurrentUserData()['id'] ?? 0);
if ($me > 0 && (int) $id === $me) return false;
PHP,
    ],
    [
        'group' => 'Authorization & privacy',
        'title' => 'Preserve protected ownership on edit',
        'event' => 'Table → Page → Before Edit Save',
        'lang' => 'php',
        'why' => 'Let administrators transfer ownership while keeping it immutable for everyone else.',
        'code' => <<<'PHP'
<?php
$user = Security::CurrentUserData();
if (($user['role'] ?? '') !== 'admin') {
    $data['created_by'] = $old['created_by'] ?? null;
}
$data['updated_by'] = (int) ($user['id'] ?? 0);
PHP,
    ],
    [
        'group' => 'Lists, search & computed data',
        'title' => 'Add a computed balance to list rows',
        'event' => 'Table → Page → After List Load',
        'lang' => 'php',
        'why' => 'Enrich displayed records without adding another stored database column.',
        'code' => <<<'PHP'
<?php
foreach ($records as &$row) {
    $row['balance'] = round(
        (float) ($row['total'] ?? 0) - (float) ($row['paid'] ?? 0),
        2
    );
}
unset($row);
PHP,
    ],
    [
        'group' => 'Lists, search & computed data',
        'title' => 'Apply a safe default search filter',
        'event' => 'Table → Page → Before Search',
        'lang' => 'php',
        'why' => 'Supply a default filter only when the visitor has not chosen one.',
        'code' => <<<'PHP'
<?php
if (empty($params['status'])) $params['status'] = 'active';
if (!empty($params['email'])) {
    $params['email'] = strtolower(trim((string) $params['email']));
}
PHP,
    ],
    [
        'group' => 'Lists, search & computed data',
        'title' => 'Highlight overdue rows',
        'event' => 'Table → Client → After List Render',
        'lang' => 'js',
        'why' => 'Use row data to add a visual warning without changing persisted values.',
        'code' => <<<'JS'
pp.grid.rows().forEach(function (row) {
    const record = pp.grid.rowData(row);
    if (record?.due_date && new Date(record.due_date) < new Date() && record.status !== 'paid') {
        row.classList.add('is-overdue');
        row.title = 'This record is overdue';
    }
});
JS,
    ],
    [
        'group' => 'Interactive forms',
        'title' => 'Enable submit only after required controls are valid',
        'event' => 'Table → Client → On Page Init',
        'lang' => 'js',
        'why' => 'Give immediate feedback while retaining server-side validation as the authority.',
        'code' => <<<'JS'
function updateSubmitState() {
    const nameOk = Runner.getControl('fullname').validate({ required: true });
    const emailOk = Runner.getControl('email').validate({ email: true });
    const save = Runner.button('save');
    if (nameOk && emailOk) save.enable();
    else save.disable();
}

Runner.getControl('fullname').on('change', updateSubmitState);
Runner.getControl('email').on('change', updateSubmitState);
updateSubmitState();
JS,
    ],
    [
        'group' => 'Interactive forms',
        'title' => 'Copy a billing address into shipping fields',
        'event' => 'Table → Client → On Page Init',
        'lang' => 'js',
        'why' => 'Keep a common data-entry shortcut local to the page.',
        'code' => <<<'JS'
Runner.getControl('same_as_billing').on('change', function (checked) {
    if (!checked) return;
    Runner.getControl('shipping_address').setValue(
        Runner.getControl('billing_address').getValue()
    );
    Runner.getControl('shipping_city').setValue(
        Runner.getControl('billing_city').getValue()
    );
});
JS,
    ],
    [
        'group' => 'Interactive forms',
        'title' => 'Warn before submitting a high-value order',
        'event' => 'Table → Client → Before Form Submit',
        'lang' => 'js',
        'why' => 'Ask for confirmation while allowing ordinary submissions to continue immediately.',
        'code' => <<<'JS'
const total = Number(Runner.getControl('total').getValue() || 0);
if (total > 10000) {
    return window.confirm('This order exceeds 10,000. Submit it now?');
}
return true;
JS,
    ],
    [
        'group' => 'Auditing & notifications',
        'title' => 'Audit successful exports',
        'event' => 'Table → Page → After Export',
        'lang' => 'php',
        'why' => 'Record who exported data, in which format, and which file was produced.',
        'code' => <<<'PHP'
<?php
$user = Security::CurrentUserData();
\PpPhp\Core\DB::Insert('security_audit', [
    'user_id' => (int) ($user['id'] ?? 0),
    'action' => 'export.' . $format,
    'details' => basename((string) $filename),
    'created_at' => date('Y-m-d H:i:s'),
]);
PHP,
    ],
    [
        'group' => 'Auditing & notifications',
        'title' => 'Notify the record owner after an update',
        'event' => 'Table → Page → After Edit Save',
        'lang' => 'php',
        'why' => 'Send a focused notification only when a meaningful field changed.',
        'code' => <<<'PHP'
<?php
if (($old['status'] ?? null) !== ($record['status'] ?? null)) {
    \PpPhp\Core\Notification::AddForUsers([(int) ($record['created_by'] ?? 0)], [
        'title' => 'Status updated',
        'message' => 'Your record is now ' . ($record['status'] ?? 'updated') . '.',
        'type' => 'info',
    ]);
}
PHP,
    ],
    [
        'group' => 'Auditing & notifications',
        'title' => 'Log a compact before-and-after snapshot',
        'event' => 'Table → Page → After Edit Save',
        'lang' => 'php',
        'why' => 'Keep an auditable record without storing passwords, tokens, or unchanged fields.',
        'code' => <<<'PHP'
<?php
$before = $old;
$after = $record;
unset($before['password'], $after['password'], $before['token'], $after['token']);
$changes = array_diff_assoc($after, $before);
if ($changes) {
    error_log('record=' . $id . ' actor=' . (Security::CurrentUsername() ?: 'system')
        . ' changes=' . json_encode($changes));
}
PHP,
    ],
];

$evhRecipeGroups = array_values(array_unique(array_column($evhRecipes, 'group')));
?>
<style>
    .evh-scrim {
        --evh-bg: var(--app-surface, #fff);
        --evh-bg-soft: var(--app-surface-muted, #f8fafc);
        --evh-panel: var(--app-panel, #fff);
        --evh-border: var(--app-border, #dbe4f0);
        --evh-text: var(--app-text, #172033);
        --evh-muted: var(--app-text-muted, #64748b);
        --evh-accent: var(--app-accent, #6d28d9);
        --evh-accent-soft: color-mix(in srgb, var(--evh-accent) 12%, var(--evh-bg));
        --evh-code-bg: #111827;
        --evh-code-text: #e5e7eb;
        position: fixed; inset: 0; z-index: 10050; display: none; opacity: 0;
        background: rgba(2, 6, 23, .64); backdrop-filter: blur(4px);
        transition: opacity .18s ease;
    }
    .evh-scrim.open { display: block; opacity: 1; }
    .evh-modal {
        position: fixed; z-index: 10051; inset: 2.5vh auto 2.5vh 50%;
        width: min(1240px, 96vw); transform: translateX(-50%) translateY(12px);
        display: flex; flex-direction: column; overflow: hidden; color: var(--evh-text);
        background: var(--evh-bg); border: 1px solid var(--evh-border); border-radius: 18px;
        box-shadow: 0 32px 90px rgba(2, 6, 23, .52); opacity: 0;
        transition: transform .2s ease, opacity .2s ease;
    }
    .evh-scrim.open .evh-modal { opacity: 1; transform: translateX(-50%) translateY(0); }
    .evh-head {
        display: flex; align-items: center; gap: 13px; flex: 0 0 auto; min-height: 72px;
        padding: 13px 18px 13px 20px; color: #fff;
        background: linear-gradient(120deg, #172554, #4338ca 58%, #7c3aed);
    }
    .evh-head-icon {
        width: 42px; height: 42px; display: grid; place-items: center; flex: 0 0 42px;
        border: 1px solid rgba(255,255,255,.25); border-radius: 12px;
        background: rgba(255,255,255,.14); font-size: 19px;
    }
    .evh-head h2 { margin: 0; font-size: 18px; font-weight: 850; letter-spacing: -.02em; }
    .evh-head p { margin: 2px 0 0; font-size: 12px; opacity: .84; }
    .evh-head-metrics { display: flex; gap: 6px; margin-left: auto; }
    .evh-head-metrics span {
        padding: 5px 8px; border: 1px solid rgba(255,255,255,.22); border-radius: 999px;
        background: rgba(255,255,255,.1); font-size: 10px; font-weight: 750;
    }
    .evh-close {
        width: 36px; height: 36px; display: grid; place-items: center; margin-left: 5px;
        border: 1px solid rgba(255,255,255,.26); border-radius: 10px; color: #fff;
        background: rgba(255,255,255,.12); cursor: pointer; font-size: 17px;
    }
    .evh-close:hover { background: rgba(255,255,255,.24); }
    .evh-body { display: grid; grid-template-columns: 248px minmax(0, 1fr); flex: 1; min-height: 0; }
    .evh-toc {
        overflow-y: auto; padding: 13px 9px 24px; border-right: 1px solid var(--evh-border);
        background: var(--evh-bg-soft);
    }
    .evh-search { position: sticky; top: 0; z-index: 2; padding: 2px 5px 10px; background: var(--evh-bg-soft); }
    .evh-search label {
        display: flex; align-items: center; gap: 8px; padding: 8px 10px;
        border: 1px solid var(--evh-border); border-radius: 9px; background: var(--evh-bg);
    }
    .evh-search input { width: 100%; min-width: 0; border: 0; outline: 0; color: var(--evh-text); background: transparent; font-size: 12px; }
    .evh-toc-group {
        padding: 13px 12px 5px; color: var(--evh-muted); font-size: 9px;
        font-weight: 850; letter-spacing: .1em; text-transform: uppercase;
    }
    .evh-toc a {
        display: flex; align-items: center; gap: 9px; margin: 1px 3px; padding: 7px 10px;
        border-left: 3px solid transparent; border-radius: 8px; color: var(--evh-muted);
        text-decoration: none; font-size: 12px;
    }
    .evh-toc a:hover, .evh-toc a.active { color: var(--evh-accent); background: var(--evh-accent-soft); }
    .evh-toc a.active { border-left-color: var(--evh-accent); font-weight: 750; }
    .evh-toc a i { width: 15px; text-align: center; color: var(--evh-accent); }
    .evh-content { overflow-y: auto; padding: 5px 32px 80px; scroll-behavior: smooth; }
    .evh-content section { padding-top: 26px; scroll-margin-top: 8px; }
    .evh-kicker { color: var(--evh-accent); font-size: 9px; font-weight: 850; letter-spacing: .12em; text-transform: uppercase; }
    .evh-content h3 { display: flex; align-items: center; gap: 9px; margin: 5px 0 6px; color: var(--evh-text); font-size: 21px; letter-spacing: -.025em; }
    .evh-content h4 { margin: 20px 0 7px; color: var(--evh-text); font-size: 14px; }
    .evh-content p, .evh-content li { color: var(--evh-muted); font-size: 13px; line-height: 1.64; }
    .evh-content ul, .evh-content ol { margin: 8px 0; padding-left: 21px; }
    .evh-content code { padding: 2px 5px; border-radius: 5px; color: var(--evh-accent); background: var(--evh-accent-soft); font: 11.5px ui-monospace, SFMono-Regular, Menlo, monospace; }
    .evh-lead { max-width: 880px; font-size: 14.5px !important; }
    .evh-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 12px 0; }
    .evh-grid.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
    .evh-card {
        padding: 14px 15px; border: 1px solid var(--evh-border); border-radius: 11px;
        background: var(--evh-panel);
    }
    .evh-card strong { display: block; margin-bottom: 4px; color: var(--evh-text); font-size: 13px; }
    .evh-card p { margin: 0; font-size: 12.3px; }
    .evh-callout {
        display: flex; gap: 10px; margin: 13px 0; padding: 12px 14px;
        border: 1px solid color-mix(in srgb, var(--evh-accent) 35%, var(--evh-border));
        border-radius: 10px; color: var(--evh-muted); background: var(--evh-accent-soft);
        font-size: 12.5px; line-height: 1.55;
    }
    .evh-callout i { color: var(--evh-accent); margin-top: 2px; }
    .evh-flow { display: grid; grid-template-columns: repeat(5, 1fr); gap: 7px; margin: 13px 0; }
    .evh-flow div { position: relative; padding: 11px 10px; border: 1px solid var(--evh-border); border-radius: 9px; background: var(--evh-panel); }
    .evh-flow b { display: block; color: var(--evh-accent); font-size: 10px; }
    .evh-flow span { color: var(--evh-muted); font-size: 10.5px; }
    .evh-filterbar { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; }
    .evh-filter {
        padding: 6px 10px; border: 1px solid var(--evh-border); border-radius: 999px;
        color: var(--evh-muted); background: var(--evh-panel); cursor: pointer; font-size: 11px; font-weight: 750;
    }
    .evh-filter.active { color: var(--evh-accent); border-color: var(--evh-accent); background: var(--evh-accent-soft); }
    .evh-recipe { margin: 10px 0; border: 1px solid var(--evh-border); border-radius: 11px; overflow: hidden; background: var(--evh-panel); }
    .evh-recipe summary { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 11px 13px; cursor: pointer; list-style: none; }
    .evh-recipe summary::-webkit-details-marker { display: none; }
    .evh-recipe summary > i { width: 30px; height: 30px; display: grid; place-items: center; border-radius: 8px; color: var(--evh-accent); background: var(--evh-accent-soft); }
    .evh-recipe-title { color: var(--evh-text); font-size: 12.5px; font-weight: 800; }
    .evh-recipe-event { margin-top: 2px; color: var(--evh-muted); font-size: 10.5px; }
    .evh-lang { padding: 3px 7px; border-radius: 999px; color: var(--evh-accent); background: var(--evh-accent-soft); font: 800 9px ui-monospace, monospace; text-transform: uppercase; }
    .evh-recipe-body { padding: 0 13px 13px; border-top: 1px solid var(--evh-border); }
    .evh-recipe-body > p { margin: 10px 2px 7px; }
    .evh-content pre { position: relative; overflow-x: auto; margin: 8px 0 0; padding: 14px 15px; border: 1px solid #334155; border-radius: 9px; color: var(--evh-code-text); background: var(--evh-code-bg); font: 11.5px/1.58 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: pre; }
    .evh-content pre code { padding: 0; color: inherit; background: transparent; font: inherit; }
    .evh-code-tools { position: absolute; top: 7px; right: 7px; display: flex; gap: 5px; opacity: 0; transition: opacity .15s; }
    pre:hover .evh-code-tools, .evh-code-tools:focus-within { opacity: 1; }
    .evh-code-btn { display: inline-flex; align-items: center; gap: 4px; padding: 5px 7px; border: 1px solid #475569; border-radius: 6px; color: #e2e8f0; background: #1e293b; cursor: pointer; font-size: 9.5px; font-weight: 750; }
    .evh-code-btn.ok { color: #86efac; border-color: #166534; background: #052e16; }
    .evh-reference { margin: 9px 0; border: 1px solid var(--evh-border); border-radius: 10px; background: var(--evh-panel); }
    .evh-reference > summary { padding: 10px 12px; color: var(--evh-text); cursor: pointer; font-size: 12.5px; font-weight: 800; }
    .evh-ref-body { padding: 0 12px 12px; }
    .evh-table-wrap { overflow-x: auto; }
    .evh-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 11.5px; }
    .evh-table th, .evh-table td { padding: 8px 9px; vertical-align: top; border-bottom: 1px solid var(--evh-border); text-align: left; }
    .evh-table th { color: var(--evh-muted); background: var(--evh-bg-soft); font-size: 9px; text-transform: uppercase; letter-spacing: .07em; }
    .evh-table td { color: var(--evh-muted); }
    .evh-table td:first-child { color: var(--evh-text); font-weight: 750; white-space: nowrap; }
    .evh-event-example { margin-top: 7px; }
    .evh-api-list { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
    .evh-api-item { padding: 9px 10px; border: 1px solid var(--evh-border); border-radius: 8px; }
    .evh-api-item b { color: var(--evh-text); font: 750 11.5px ui-monospace, monospace; }
    .evh-api-item small { display: block; margin-top: 3px; color: var(--evh-muted); font-size: 10.5px; line-height: 1.45; }
    .evh-checklist { list-style: none; padding: 0 !important; }
    .evh-checklist li { position: relative; padding-left: 22px; }
    .evh-checklist li::before { content: "✓"; position: absolute; left: 0; color: #10b981; font-weight: 900; }
    .evh-empty { display: none; padding: 22px; color: var(--evh-muted); text-align: center; }
    [data-guide-search].evh-hidden, .evh-recipe.evh-hidden { display: none !important; }
    @media (max-width: 880px) {
        .evh-modal { top: 1vh; bottom: 1vh; width: 98vw; }
        .evh-body { grid-template-columns: 1fr; }
        .evh-toc { max-height: 166px; border-right: 0; border-bottom: 1px solid var(--evh-border); }
        .evh-content { padding: 4px 16px 60px; }
        .evh-grid, .evh-grid.three, .evh-api-list { grid-template-columns: 1fr; }
        .evh-flow { grid-template-columns: 1fr 1fr; }
        .evh-head-metrics { display: none; }
    }
</style>

<div class="evh-scrim pp-legacy-guide" id="evHelpScrim" aria-hidden="true">
    <article class="evh-modal" role="dialog" aria-modal="true" aria-labelledby="evHelpTitle">
        <header class="evh-head">
            <div class="evh-head-icon"><i class="bi bi-lightning-charge"></i></div>
            <div>
                <h2 id="evHelpTitle">Events — practical handbook & complete reference</h2>
                <p>Choose the right hook, understand its contract, and build production-ready workflows.</p>
            </div>
            <div class="evh-head-metrics" aria-hidden="true">
                <span><?= $evhH($evhTotalEvents) ?> events</span>
                <span><?= $evhH(count($evhRecipes)) ?> recipes</span>
                <span><?= $evhH(count($evhPhpApi) + count($evhJsApi)) ?> APIs</span>
            </div>
            <button class="evh-close" type="button" data-ev-help-close aria-label="Close Events guide"><i class="bi bi-x-lg"></i></button>
        </header>

        <div class="evh-body">
            <nav class="evh-toc" aria-label="Events guide contents">
                <div class="evh-search">
                    <label><i class="bi bi-search"></i><input id="evHelpSearch" type="search" placeholder="Search guide, event or API…" autocomplete="off"></label>
                </div>
                <div class="evh-toc-group">Start here</div>
                <a href="#evh-overview"><i class="bi bi-compass"></i> Mental model</a>
                <a href="#evh-first"><i class="bi bi-play-circle"></i> First event</a>
                <a href="#evh-choose"><i class="bi bi-signpost-split"></i> Which event?</a>
                <a href="#evh-contracts"><i class="bi bi-arrow-return-left"></i> Arguments & returns</a>
                <a href="#evh-editor"><i class="bi bi-stars"></i> Editor & IntelliSense</a>
                <div class="evh-toc-group">Learn by example</div>
                <a href="#evh-cookbook"><i class="bi bi-journal-code"></i> Recipe cookbook</a>
                <a href="#evh-bridge"><i class="bi bi-arrow-left-right"></i> Browser ↔ server</a>
                <div class="evh-toc-group">Authoritative reference</div>
                <a href="#evh-global"><i class="bi bi-globe2"></i> Global events</a>
                <a href="#evh-table"><i class="bi bi-table"></i> Table events</a>
                <a href="#evh-field"><i class="bi bi-input-cursor-text"></i> Field events</a>
                <a href="#evh-php-api"><i class="bi bi-filetype-php"></i> PHP APIs</a>
                <a href="#evh-js-api"><i class="bi bi-filetype-js"></i> JavaScript APIs</a>
                <div class="evh-toc-group">Production practice</div>
                <a href="#evh-debug"><i class="bi bi-bug"></i> Debugging</a>
                <a href="#evh-safety"><i class="bi bi-shield-check"></i> Security & performance</a>
                <a href="#evh-tips"><i class="bi bi-lightbulb"></i> Tips, tricks & FAQ</a>
            </nav>

            <main class="evh-content" id="evHelpContent">
                <section id="evh-overview" data-guide-search="mental model scope global table field server browser php javascript">
                    <div class="evh-kicker">Start here</div>
                    <h3>Events are precise extension points</h3>
                    <p class="evh-lead">An event is code that runs at a defined moment in an application lifecycle. Choose the narrowest scope that owns the behavior, write against the arguments shown by the editor, and use the return contract to continue, modify, redirect, or cancel the operation.</p>
                    <div class="evh-grid three">
                        <div class="evh-card"><strong><i class="bi bi-globe2"></i> Global events</strong><p>Authentication, application startup, menus, email, files, APIs, webhooks and real-time behavior shared by the project.</p></div>
                        <div class="evh-card"><strong><i class="bi bi-table"></i> Table events</strong><p>List, Add, Edit, View, Delete, import/export, SQL filters and page JavaScript for one table or view.</p></div>
                        <div class="evh-card"><strong><i class="bi bi-input-cursor-text"></i> Field events</strong><p>One field’s formatting, calculation, validation, normalization and browser interactions.</p></div>
                    </div>
                    <div class="evh-flow">
                        <div><b>1 · Choose scope</b><span>Global, Table or Field</span></div>
                        <div><b>2 · Choose moment</b><span>Before, after, failure or client</span></div>
                        <div><b>3 · Read contract</b><span>Arguments and return behavior</span></div>
                        <div><b>4 · Implement</b><span>Use IntelliSense and Check</span></div>
                        <div><b>5 · Verify</b><span>Save, Build and test both paths</span></div>
                    </div>
                    <div class="evh-callout"><i class="bi bi-shield-lock"></i><span><b>Rule of trust:</b> JavaScript improves the experience; PHP enforces business and security rules. Any critical browser validation must have a server equivalent.</span></div>
                </section>

                <section id="evh-first" data-guide-search="first workflow enable edit check save build test">
                    <div class="evh-kicker">Five-minute workflow</div>
                    <h3>Create and verify your first event</h3>
                    <ol>
                        <li>Open <b>Events</b>, then expand <b>Global Events</b> or find a table under <b>Table Events</b>.</li>
                        <li>Select an event. Read its description, arguments and return value before editing.</li>
                        <li>Turn the event on. Start from the generated stub—it uses the exact variables supported by that hook.</li>
                        <li>Use <b>Ctrl/Cmd + Space</b> for APIs and schema fields. Run <b>Check</b>, then <b>Save</b>.</li>
                        <li><b>Build</b> the generated app and test the normal path, rejection path, and failure path.</li>
                    </ol>
                    <div class="evh-grid">
                        <div class="evh-card"><strong>Before hooks</strong><p>Use for validation, authorization, defaults, normalization and cancellation. They run before the main action.</p></div>
                        <div class="evh-card"><strong>After hooks</strong><p>Use for related rows, notifications, audit logs and cleanup. They run after success unless the event says otherwise.</p></div>
                    </div>
                </section>

                <section id="evh-choose" data-guide-search="decision choose event before after global table field client sql">
                    <div class="evh-kicker">Decision guide</div>
                    <h3>Which event should I use?</h3>
                    <div class="evh-table-wrap"><table class="evh-table">
                        <thead><tr><th>Goal</th><th>Best starting point</th><th>Why</th></tr></thead>
                        <tbody>
                            <tr><td>Reject or modify data before save</td><td><code>beforeAddSave</code> / <code>beforeEditSave</code></td><td>Has submitted data and a documented cancel/error contract.</td></tr>
                            <tr><td>React after a successful save</td><td><code>afterAddSave</code> / <code>afterEditSave</code></td><td>Record/id exist and the primary write succeeded.</td></tr>
                            <tr><td>Hide, show or link form controls</td><td><code>onPageInit</code></td><td>The browser controls are mounted and Runner is available.</td></tr>
                            <tr><td>Validate one field everywhere</td><td>Field <code>validate</code></td><td>One rule applies consistently across entry points.</td></tr>
                            <tr><td>Restrict visible database rows</td><td><code>sqlWhere</code></td><td>Filtering happens in SQL before rows leave the database.</td></tr>
                            <tr><td>Change displayed list records</td><td><code>afterListLoad</code></td><td>Use for derived display values, not security filtering.</td></tr>
                            <tr><td>Change project-wide authentication</td><td>Global Authentication events</td><td>One policy covers every protected page.</td></tr>
                            <tr><td>Protect files, APIs or webhooks</td><td>Matching Global integration event</td><td>Runs at the shared system boundary.</td></tr>
                            <tr><td>Customize import/export/print</td><td>Table Page event</td><td>Keeps behavior attached to the affected data source.</td></tr>
                        </tbody>
                    </table></div>
                    <div class="evh-callout"><i class="bi bi-info-circle"></i><span>If two events could work, prefer the narrower one and the earliest safe moment. Example: use field <b>Before Set</b> for reusable normalization, but table <b>Before Add Save</b> for a rule involving several fields.</span></div>
                </section>

                <section id="evh-contracts" data-guide-search="arguments by reference returns false url array cancel redirect modify void">
                    <div class="evh-kicker">The event contract</div>
                    <h3>Arguments, references and return values</h3>
                    <div class="evh-grid">
                        <div class="evh-card"><strong>Arguments are event-specific</strong><p>Use only the variables listed for the selected event. <code>$data</code> in one hook does not imply it exists in another.</p></div>
                        <div class="evh-card"><strong>By-reference values are editable</strong><p>Descriptions such as “modify by reference” mean assigning into the array changes downstream behavior.</p></div>
                        <div class="evh-card"><strong><code>false</code> usually cancels</strong><p>Only return it when the event contract explicitly documents cancellation or rejection.</p></div>
                        <div class="evh-card"><strong>Errors and redirects are structured</strong><p>Validation hooks accept field-keyed error arrays; navigation hooks may accept a URL. Follow the exact reference.</p></div>
                    </div>
                    <pre><code>&lt;?php
// Field-keyed validation error:
return ['email' =&gt; 'Please enter a work email address.'];

// Cancel only when the selected event documents false:
return false;

// Continue normally:
return null;</code></pre>
                </section>

                <section id="evh-editor" data-guide-search="editor intellisense autocomplete variables schema field names runner getControl check copy insert">
                    <div class="evh-kicker">Write faster, make fewer mistakes</div>
                    <h3>Editor, IntelliSense and schema-aware fields</h3>
                    <ul class="evh-checklist">
                        <li>Press <b>Ctrl/Cmd + Space</b> to open completions for event variables, PHP helpers, <code>pp.*</code>, <code>Runner.*</code> and schema fields.</li>
                        <li>After <code>Runner.getControl('</code>, choose from the fields available to the current table/view instead of typing a guess.</li>
                        <li>Hover a completion to read its signature and behavior before inserting it.</li>
                        <li>Use <b>Check</b> before saving. It catches syntax and several unsupported patterns; runtime tests still matter.</li>
                        <li>Every code sample in this Guide has <b>Copy</b>. When an event is open, <b>Insert</b> places the sample into the editor.</li>
                    </ul>
                    <div class="evh-callout"><i class="bi bi-stars"></i><span>The Guide’s event and API directories below come from the same PHP catalogues as IntelliSense. Adding a supported event or helper updates both surfaces automatically.</span></div>
                </section>

                <section id="evh-cookbook" data-guide-search="cookbook recipes examples forms validation list workflow authentication email file api realtime import export print">
                    <div class="evh-kicker">Copy-ready patterns</div>
                    <h3>Recipe cookbook — <?= $evhH(count($evhRecipes)) ?> practical examples</h3>
                    <p>Filter by outcome, open a recipe, then adapt table names, field names and policies to your project. Examples favor explicit, readable code suitable for a first implementation.</p>
                    <div class="evh-filterbar" role="group" aria-label="Filter cookbook">
                        <button class="evh-filter active" type="button" data-recipe-filter="all">All recipes</button>
                        <?php foreach ($evhRecipeGroups as $group): ?>
                            <button class="evh-filter" type="button" data-recipe-filter="<?= $evhH($group) ?>"><?= $evhH($group) ?></button>
                        <?php endforeach; ?>
                    </div>
                    <div id="evhRecipeList">
                        <?php foreach ($evhRecipes as $index => $recipe): ?>
                            <details class="evh-recipe" data-recipe-group="<?= $evhH($recipe['group']) ?>" data-guide-search="<?= $evhH(implode(' ', [$recipe['group'], $recipe['title'], $recipe['event'], $recipe['why'], $recipe['code']])) ?>">
                                <summary>
                                    <i class="bi bi-code-square"></i>
                                    <span><span class="evh-recipe-title"><?= $evhH(($index + 1) . '. ' . $recipe['title']) ?></span><span class="evh-recipe-event"><?= $evhH($recipe['event']) ?></span></span>
                                    <span class="evh-lang"><?= $evhH($recipe['lang']) ?></span>
                                </summary>
                                <div class="evh-recipe-body">
                                    <p><?= $evhH($recipe['why']) ?></p>
                                    <pre data-ev-code><code><?= $evhH($recipe['code']) ?></code></pre>
                                </div>
                            </details>
                        <?php endforeach; ?>
                    </div>
                </section>

                <section id="evh-bridge" data-guide-search="browser server bridge pp.run custom function command runner">
                    <div class="evh-kicker">Advanced workflow</div>
                    <h3>Bridge browser interaction to trusted server work</h3>
                    <p>Use JavaScript to gather context and call a named server action with <code>pp.run()</code>. Perform permission checks and database work on the server, then return only the data needed by the page.</p>
                    <div class="evh-grid">
                        <div class="evh-card"><strong>Browser event</strong><p>Reads controls, confirms intent, shows progress and renders the result.</p></div>
                        <div class="evh-card"><strong>Server action / Custom Function</strong><p>Checks the current user, validates parameters, uses prepared queries and returns a small serializable result.</p></div>
                    </div>
                    <pre data-ev-code><code>// Browser event
const response = await pp.run('checkCustomerCredit', {
    customerId: Runner.getControlValue('customer_id')
});
Runner.setControlValue('credit_status', response.status);</code></pre>
                    <div class="evh-callout"><i class="bi bi-shield-check"></i><span>Never authorize an action because JavaScript sent a role, price, owner id or “approved” flag. Re-read trusted state and permissions on the server.</span></div>
                </section>

                <?php
                $evhRenderEventDirectory = static function (string $id, string $kicker, string $title, array $groups) use ($evhH): void {
                    ?>
                    <section id="<?= $evhH($id) ?>" data-guide-search="<?= $evhH($title . ' ' . json_encode($groups)) ?>">
                        <div class="evh-kicker"><?= $evhH($kicker) ?></div>
                        <h3><?= $evhH($title) ?></h3>
                        <p>Expand a category for exact event names, arguments, return contracts and editor-generated starting code.</p>
                        <?php foreach ($groups as $group): ?>
                            <details class="evh-reference">
                                <summary><i class="bi <?= $evhH($group['icon'] ?? 'bi-lightning') ?>"></i> <?= $evhH($group['label'] ?? 'Events') ?> · <?= $evhH(count($group['events'] ?? [])) ?></summary>
                                <div class="evh-ref-body evh-table-wrap">
                                    <table class="evh-table">
                                        <thead><tr><th>Event</th><th>Language</th><th>Arguments</th><th>What it does / returns</th></tr></thead>
                                        <tbody>
                                        <?php foreach (($group['events'] ?? []) as $name => $event): ?>
                                            <tr>
                                                <td><code><?= $evhH($name) ?></code><div><?= $evhH($event['label'] ?? $name) ?></div></td>
                                                <td><span class="evh-lang"><?= $evhH($event['lang'] ?? 'php') ?></span></td>
                                                <td>
                                                    <?php if (!empty($event['args'])): ?>
                                                        <?php foreach ($event['args'] as $arg => $meaning): ?><div><code><?= $evhH($arg) ?></code> — <?= $evhH($meaning) ?></div><?php endforeach; ?>
                                                    <?php else: ?>None<?php endif; ?>
                                                </td>
                                                <td>
                                                    <div><?= $evhH($event['desc'] ?? '') ?></div>
                                                    <?php if (!empty($event['returns'])): ?><div><b>Return:</b> <?= $evhH(strip_tags((string) $event['returns'])) ?></div><?php endif; ?>
                                                    <?php if (!empty($event['stub'])): ?>
                                                        <details class="evh-event-example"><summary>Starting code</summary><pre data-ev-code><code><?= $evhH($event['stub']) ?></code></pre></details>
                                                    <?php endif; ?>
                                                </td>
                                            </tr>
                                        <?php endforeach; ?>
                                        </tbody>
                                    </table>
                                </div>
                            </details>
                        <?php endforeach; ?>
                    </section>
                    <?php
                };
                $evhRenderEventDirectory('evh-global', 'Authoritative directory', 'Global event reference', $evhGlobalGroups);
                $evhRenderEventDirectory('evh-table', 'Authoritative directory', 'Table event reference', $evhTableGroups);
                $evhRenderEventDirectory('evh-field', 'Authoritative directory', 'Field event reference', $evhFieldGroups);
                ?>

                <?php
                $evhRenderApiDirectory = static function (string $id, string $title, array $categories, string $language) use ($evhH): void {
                    ?>
                    <section id="<?= $evhH($id) ?>" data-guide-search="<?= $evhH($title . ' ' . json_encode($categories)) ?>">
                        <div class="evh-kicker">IntelliSense directory</div>
                        <h3><?= $evhH($title) ?></h3>
                        <p>These are the callable helpers offered by the <?= $evhH($language) ?> editor. Expand a category for signatures, descriptions and insertable examples.</p>
                        <?php foreach ($categories as $category => $rows): ?>
                            <details class="evh-reference">
                                <summary><?= $evhH($category) ?> · <?= $evhH(count($rows)) ?></summary>
                                <div class="evh-ref-body evh-api-list">
                                    <?php foreach ($rows as $row): ?>
                                        <div class="evh-api-item" data-guide-search="<?= $evhH(implode(' ', $row)) ?>">
                                            <b><?= $evhH($row['label'] ?? '') ?></b>
                                            <small><?= $evhH($row['detail'] ?? '') ?></small>
                                            <small><?= $evhH($row['documentation'] ?? '') ?></small>
                                            <?php if (!empty($row['example'])): ?><pre data-ev-code><code><?= $evhH($row['example']) ?></code></pre><?php endif; ?>
                                        </div>
                                    <?php endforeach; ?>
                                </div>
                            </details>
                        <?php endforeach; ?>
                    </section>
                    <?php
                };
                $evhRenderApiDirectory('evh-php-api', 'PHP server API reference', $evhPhpByCategory, 'PHP');
                $evhRenderApiDirectory('evh-js-api', 'JavaScript browser API reference', $evhJsByCategory, 'JavaScript');
                ?>

                <section id="evh-debug" data-guide-search="debugging logs console error checker trace test failure">
                    <div class="evh-kicker">Find problems systematically</div>
                    <h3>Debugging events</h3>
                    <div class="evh-grid">
                        <div class="evh-card"><strong>1 · Confirm it is enabled</strong><p>The event toggle, Save result and latest Build must all reflect the current code.</p></div>
                        <div class="evh-card"><strong>2 · Confirm the hook fires</strong><p>Temporarily add <code>error_log()</code> in PHP or <code>console.debug()</code> in JavaScript.</p></div>
                        <div class="evh-card"><strong>3 · Inspect actual values</strong><p>Log only safe identifiers and types. Avoid passwords, tokens, private keys and full personal records.</p></div>
                        <div class="evh-card"><strong>4 · Test both outcomes</strong><p>Exercise success, rejection, empty data, permission failure and provider/network failure.</p></div>
                    </div>
                    <pre data-ev-code><code>&lt;?php
error_log('[orders.beforeAddSave] user=' . (string) (Security::CurrentUserData()['id'] ?? 0)
    . ' fields=' . implode(',', array_keys($data)));</code></pre>
                    <pre data-ev-code><code>console.debug('[customers.onPageInit]', {
    pageId: pp.page.id,
    available: Object.keys(Runner.getData() || {})
});</code></pre>
                    <p><b>Nothing happens?</b> Verify that the code is in the correct table and event, the generated application was rebuilt, the page is not serving an old browser cache, and the field name matches the physical schema name shown by IntelliSense.</p>
                </section>

                <section id="evh-safety" data-guide-search="security performance transaction prepared query validation authorization secrets xss csrf">
                    <div class="evh-kicker">Production checklist</div>
                    <h3>Security, correctness and performance</h3>
                    <ul class="evh-checklist">
                        <li>Use prepared parameters for values. Never concatenate untrusted form or request text into SQL.</li>
                        <li>Enforce authorization in PHP even when controls are hidden, disabled or read-only in JavaScript.</li>
                        <li>Escape values rendered as HTML. Field Format intentionally returns HTML, so encode user-controlled text first.</li>
                        <li>Keep Before Login, SQL Where, formatting and list events fast; they run frequently and delay the page.</li>
                        <li>Make webhooks idempotent by storing a provider event id before applying a side effect.</li>
                        <li>Do not log passwords, session ids, OAuth tokens, API keys, private keys or unredacted personal data.</li>
                        <li>For multi-step database work, use the generated database transaction helpers where partial completion would corrupt state.</li>
                        <li>Return only documented types. A convenient but unsupported return can silently do nothing.</li>
                    </ul>
                </section>

                <section id="evh-tips" data-guide-search="tips tricks faq reusable naming order priorities common questions">
                    <div class="evh-kicker">Working habits</div>
                    <h3>Tips, tricks and frequently asked questions</h3>
                    <div class="evh-grid">
                        <div class="evh-card"><strong>Use the smallest scope</strong><p>A field formatter is easier to reuse than repeating formatting in every page hook.</p></div>
                        <div class="evh-card"><strong>One event, one responsibility</strong><p>Extract larger reusable operations into Custom Functions and call them from thin event handlers.</p></div>
                        <div class="evh-card"><strong>Prefer explicit field names</strong><p>Use schema-aware completions. Labels such as “Full name” are presentation; <code>fullname</code> may be the physical field.</p></div>
                        <div class="evh-card"><strong>Design for retries</strong><p>Email, webhook and real-time delivery may retry. Use unique keys or status markers to avoid duplicate side effects.</p></div>
                        <div class="evh-card"><strong>Keep display and trust separate</strong><p>Browser state is temporary. Persist or validate important state on the server.</p></div>
                        <div class="evh-card"><strong>Document the why</strong><p>Add a short comment explaining the business rule, not a line-by-line translation of the code.</p></div>
                    </div>
                    <h4>Why does code work in one event but not another?</h4>
                    <p>Each event has different arguments, timing and return behavior. Re-open the event reference and use only its listed variables and contract.</p>
                    <h4>Why did hiding a field not protect it?</h4>
                    <p>Hidden controls are a UI choice. A crafted HTTP request can still send values. Validate allowed fields and permissions in a server event.</p>
                    <h4>Should I use After List Load or SQL Where?</h4>
                    <p>Use SQL Where to restrict rows for security or scale. Use After List Load only to enrich records that were already allowed and loaded.</p>
                    <h4>How do I discover exact field names for Runner?</h4>
                    <p>Type <code>Runner.getControl('</code> and invoke IntelliSense. The editor offers fields for the current table/view, preventing label/name mismatches.</p>
                    <h4>Can I paste a Guide recipe unchanged?</h4>
                    <p>Recipes demonstrate a correct event pattern, but table names, fields, roles, URLs and policies must match your project. Run Check, then test the generated application.</p>
                </section>

                <div class="evh-empty" id="evhSearchEmpty"><i class="bi bi-search"></i> No Guide item matches that search.</div>
            </main>
        </div>
    </article>
</div>

<script>
(function () {
    'use strict';
    const scrim = document.getElementById('evHelpScrim');
    const content = document.getElementById('evHelpContent');
    const search = document.getElementById('evHelpSearch');
    if (!scrim || !content) return;

    function openGuide() {
        scrim.classList.add('open');
        scrim.setAttribute('aria-hidden', 'false');
        document.body.style.overflow = 'hidden';
        window.setTimeout(() => search && search.focus(), 120);
    }
    function closeGuide() {
        scrim.classList.remove('open');
        scrim.setAttribute('aria-hidden', 'true');
        document.body.style.overflow = '';
    }
    document.addEventListener('click', function (event) {
        if (event.target.closest('[data-ev-help-open]')) openGuide();
        if (event.target.closest('[data-ev-help-close]') || event.target === scrim) closeGuide();
    });
    document.addEventListener('keydown', function (event) {
        if (event.key === 'Escape' && scrim.classList.contains('open')) closeGuide();
    });

    const tocLinks = Array.from(scrim.querySelectorAll('.evh-toc a[href^="#"]'));
    tocLinks.forEach(function (link) {
        link.addEventListener('click', function (event) {
            event.preventDefault();
            const target = scrim.querySelector(link.getAttribute('href'));
            if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
        });
    });
    content.addEventListener('scroll', function () {
        let current = '';
        content.querySelectorAll('section[id]').forEach(function (section) {
            if (section.offsetTop <= content.scrollTop + 80) current = section.id;
        });
        tocLinks.forEach(function (link) {
            link.classList.toggle('active', link.getAttribute('href') === '#' + current);
        });
    }, { passive: true });

    function attachTools(pre) {
        if (pre.querySelector('.evh-code-tools')) return;
        const code = pre.querySelector('code');
        if (!code) return;
        const tools = document.createElement('span');
        tools.className = 'evh-code-tools';
        const copy = document.createElement('button');
        copy.type = 'button';
        copy.className = 'evh-code-btn';
        copy.innerHTML = '<i class="bi bi-copy"></i> Copy';
        copy.addEventListener('click', async function () {
            await navigator.clipboard.writeText(code.textContent || '');
            copy.classList.add('ok');
            copy.innerHTML = '<i class="bi bi-check2"></i> Copied';
            window.setTimeout(function () {
                copy.classList.remove('ok');
                copy.innerHTML = '<i class="bi bi-copy"></i> Copy';
            }, 1400);
        });
        tools.appendChild(copy);
        if (typeof window.PpEventsInsertExample === 'function') {
            const insert = document.createElement('button');
            insert.type = 'button';
            insert.className = 'evh-code-btn';
            insert.innerHTML = '<i class="bi bi-box-arrow-in-down"></i> Insert';
            insert.addEventListener('click', function () {
                window.PpEventsInsertExample(code.textContent || '');
                closeGuide();
            });
            tools.appendChild(insert);
        }
        pre.appendChild(tools);
    }
    scrim.querySelectorAll('pre').forEach(attachTools);

    const filters = Array.from(scrim.querySelectorAll('[data-recipe-filter]'));
    filters.forEach(function (button) {
        button.addEventListener('click', function () {
            const value = button.dataset.recipeFilter || 'all';
            filters.forEach(item => item.classList.toggle('active', item === button));
            scrim.querySelectorAll('.evh-recipe').forEach(function (recipe) {
                recipe.classList.toggle('evh-hidden', value !== 'all' && recipe.dataset.recipeGroup !== value);
            });
        });
    });

    if (search) {
        search.addEventListener('input', function () {
            const term = search.value.trim().toLowerCase();
            let visible = 0;
            scrim.querySelectorAll('[data-guide-search]').forEach(function (item) {
                const matches = !term || (item.dataset.guideSearch || '').toLowerCase().includes(term);
                item.classList.toggle('evh-hidden', !matches);
                if (matches) visible++;
            });
            document.getElementById('evhSearchEmpty').style.display = visible ? 'none' : 'block';
        });
    }
})();
</script>
