JezK
Edit File: functions.php
<?php function h(?string $str): string { return htmlspecialchars($str ?? '', ENT_QUOTES, 'UTF-8'); } function format_currency(float $amount): string { return '₹' . number_format($amount, 2); } function generate_csrf_token(): string { if (empty($_SESSION[CSRF_TOKEN_NAME])) { $_SESSION[CSRF_TOKEN_NAME] = bin2hex(random_bytes(32)); } return $_SESSION[CSRF_TOKEN_NAME]; } function verify_csrf_token(?string $token): bool { if (empty($_SESSION[CSRF_TOKEN_NAME]) || empty($token)) { return false; } return hash_equals($_SESSION[CSRF_TOKEN_NAME], $token); } function sanitize_input(string $input): string { return htmlspecialchars(strip_tags(trim($input)), ENT_QUOTES, 'UTF-8'); } function set_flash(string $type, string $message): void { $_SESSION['flash'] = ['type' => $type, 'message' => $message]; } function get_flash(): ?array { if (isset($_SESSION['flash'])) { $flash = $_SESSION['flash']; unset($_SESSION['flash']); return $flash; } return null; } function render_flash(): string { $flash = get_flash(); if (!$flash) return ''; $icon = match($flash['type']) { 'success' => '✅', 'error' => '❌', 'warning' => '⚠️', default => 'ℹ️', }; return '<div class="alert alert-' . $flash['type'] . '">' . '<span class="alert-icon">' . $icon . '</span>' . '<span>' . htmlspecialchars($flash['message']) . '</span>' . '</div>'; } function get_setting(string $key, $default = null) { try { $pdo = db(); $stmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = ?"); $stmt->execute([$key]); $val = $stmt->fetchColumn(); return $val !== false ? $val : $default; } catch (Exception $e) { return $default; } } function set_setting(string $key, $value): bool { try { $pdo = db(); $stmt = $pdo->prepare("UPDATE settings SET setting_value = ? WHERE setting_key = ?"); return $stmt->execute([$value, $key]); } catch (Exception $e) { return false; } } function create_anomaly_alert(?int $lab_id, string $alert_type, string $severity, string $description, array $reference_data = []): void { try { $pdo = db(); $stmt = $pdo->prepare(" INSERT INTO anomaly_alerts (lab_id, alert_type, severity, description, reference_data) VALUES (?, ?, ?, ?, ?) "); $stmt->execute([$lab_id, $alert_type, $severity, $description, json_encode($reference_data)]); } catch (Exception $e) { error_log('Anomaly alert failed: ' . $e->getMessage()); } } /** * Validates if the requested sittings are within the defined limits for the procedure. * Returns true if valid, or an error message if invalid. */ function validate_treatment_plan(int $procedure_id, int $sittings): string|bool { try { $pdo = db(); $stmt = $pdo->prepare("SELECT name, min_sittings, max_sittings FROM procedures WHERE id = ?"); $stmt->execute([$procedure_id]); $proc = $stmt->fetch(); if (!$proc) return "Invalid procedure selected."; if ($sittings < $proc['min_sittings']) { return "Minimum sittings required for " . $proc['name'] . " is " . $proc['min_sittings'] . "."; } if ($sittings > $proc['max_sittings']) { return "Maximum sittings allowed for " . $proc['name'] . " is " . $proc['max_sittings'] . "."; } return true; } catch (Exception $e) { return "Validation error."; } } function trigger_alert_email(string $subject, string $body, string $severity = 'medium', ?int $lab_id = null): bool { if (!defined('ENABLE_EMAIL_ALERTS') || !ENABLE_EMAIL_ALERTS) return false; $to = defined('ALERT_EMAIL_RECIPIENT') ? ALERT_EMAIL_RECIPIENT : ''; if (empty($to)) return false; $headers = "From: DentalCare Monitor <alerts@example.com>\r\n"; $headers .= "MIME-Version: 1.0\r\n"; $headers .= "Content-Type: text/html; charset=UTF-8\r\n"; $htmlBody = " <div style='font-family:sans-serif; border:1px solid #eee; padding:20px; border-radius:10px;'> <h2 style='color:" . ($severity === 'high' ? '#ef4444' : '#f97316') . ";'>Critical System Alert</h2> <p><strong>Type:</strong> $subject</p> <p><strong>Severity:</strong> " . strtoupper($severity) . "</p> <p><strong>Description:</strong> $body</p> <hr> <p style='font-size:12px; color:#666;'>This is an automated alert from the DentalCare ERP Surveillance System.</p> </div>"; // Standard PHP mail() call // Note: Requires SMTP configured on server error_log("[MOCKED SMTP EMAIL] To: $to | Subject: $subject | Severity: $severity"); return @mail($to, "[ALERT] " . $subject, $htmlBody, $headers); } function send_whatsapp_message(string $phone, string $message, ?int $patient_id = null, string $type = 'notification'): bool { $status = 'pending'; $err = null; if (defined('TWILIO_SID') && TWILIO_SID !== 'REPLACE_WITH_TWILIO_SID') { $sid = TWILIO_SID; $token = TWILIO_AUTH_TOKEN; $from = TWILIO_WHATSAPP_FROM; $to = 'whatsapp:+91' . preg_replace('/[^0-9]/', '', $phone); $url = "https://api.twilio.com/2010-04-01/Accounts/$sid/Messages.json"; $data = http_build_query(['From' => $from, 'To' => $to, 'Body' => $message]); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $data, CURLOPT_USERPWD => "$sid:$token", CURLOPT_TIMEOUT => 10, ]); $resp = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); $rd = json_decode($resp, true); if ($http_code === 201 && isset($rd['sid'])) { $status = 'sent'; } else { $status = 'failed'; $err = $rd['message'] ?? 'Unknown Twilio error'; } } else { $status = 'pending'; $err = 'Twilio credentials not configured'; } // Log to database try { $pdo = db(); $pdo->prepare("INSERT INTO whatsapp_logs (patient_id, phone_number, message_type, message_body, status) VALUES (?, ?, ?, ?, ?)") ->execute([$patient_id, $phone, $type, $message, $status]); return $status === 'sent'; } catch (Exception $e) { error_log('WhatsApp logging failed: ' . $e->getMessage()); return false; } } // ─── Low Stock Auto-Alert ─────────────────────────────────────────────────── function check_low_stock_alerts(int $inventory_id): void { try { $pdo = db(); $item = $pdo->prepare("SELECT i.*, l.name as lab_name FROM inventory i JOIN labs l ON l.id=i.lab_id WHERE i.id=?"); $item->execute([$inventory_id]); $it = $item->fetch(); if (!$it) return; if ((float)$it['quantity'] <= (float)$it['min_threshold']) { $dup = $pdo->prepare("SELECT id FROM anomaly_alerts WHERE alert_type='Low Stock' AND JSON_UNQUOTE(JSON_EXTRACT(reference_data, '$.inventory_id'))=? AND is_resolved=0"); $dup->execute([$inventory_id]); if (!$dup->fetchColumn()) { create_anomaly_alert( (int)$it['lab_id'], 'Low Stock', $it['quantity'] <= 0 ? 'high' : 'medium', "{$it['lab_name']}: {$it['name']} is " . ($it['quantity'] <= 0 ? 'OUT OF STOCK' : 'LOW (qty: ' . $it['quantity'] . ' ' . $it['unit'] . ')') . '. Min threshold: ' . $it['min_threshold'], ['inventory_id' => $inventory_id, 'item' => $it['name']] ); } } } catch (Exception $e) { error_log('Low stock check failed: ' . $e->getMessage()); } } /** * Logs a system action for audit purposes. * Requirement 2: Each action must be linked to user ID and recorded in audit logs. */ function log_audit(string $action, ?string $table = null, ?int $record_id = null, $old_values = null, $new_values = null): void { try { $pdo = db(); $user_id = $_SESSION['user_id'] ?? 0; if ($user_id === 0) return; // Don't log anonymous actions if user_id is missing $stmt = $pdo->prepare(" INSERT INTO audit_logs (user_id, action, table_name, record_id, old_values, new_values, ip_address) VALUES (?, ?, ?, ?, ?, ?, ?) "); $old_json = $old_values ? json_encode($old_values) : null; $new_json = $new_values ? json_encode($new_values) : null; $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; $stmt->execute([ $user_id, $action, $table, $record_id, $old_json, $new_json, $ip ]); } catch (Exception $e) { error_log('Audit logging failed: ' . $e->getMessage()); } }