File manager - Edit - /home/webtrixia/appvetta.webtrixia.com/uploads/views/api.php
Back
<?php /** * VETTA Mobile API * All endpoints: /api/* */ define('ROOT', __DIR__); require ROOT.'/config/config.php'; require ROOT.'/src/helpers/Core.php'; require ROOT.'/src/helpers/JWT.php'; // CORS for React Native header('Access-Control-Allow-Origin: *'); header('Access-Control-Allow-Methods: GET, POST, OPTIONS'); header('Access-Control-Allow-Headers: Authorization, Content-Type'); header('Content-Type: application/json; charset=utf-8'); if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(204); exit; } // JSON response helper function api(array $data, int $code = 200): void { http_response_code($code); echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } // Auth middleware function apiAuth(): object { $payload = JWT::fromRequest(); if (!$payload) api(['ok'=>false,'msg'=>'Unauthorized'], 401); $user = DB::row('SELECT * FROM users WHERE id=? AND is_banned=0', [$payload['uid']]); if (!$user) api(['ok'=>false,'msg'=>'User not found'], 401); return $user; } // Get POST body (JSON or form) function body(): array { $json = json_decode(file_get_contents('php://input'), true); return $json ?? $_POST; } function hasDeviceTokensTable(): bool { return DB::tableExists('device_tokens'); } function hasNotificationPrefsTable(): bool { return DB::tableExists('notification_preferences'); } function saveDeviceToken(int $userId, string $token, string $platform = 'web', ?string $appVersion = null, ?string $locale = null): void { $token = trim($token); if ($token === '') return; if (hasDeviceTokensTable()) { DB::run( 'INSERT INTO device_tokens (user_id, platform, push_token, app_version, locale, is_active, last_seen_at) VALUES (?,?,?,?,?,1,NOW()) ON DUPLICATE KEY UPDATE user_id=VALUES(user_id), platform=VALUES(platform), app_version=VALUES(app_version), locale=VALUES(locale), is_active=1, last_seen_at=NOW()', [$userId, $platform ?: 'web', $token, $appVersion, $locale] ); } if (DB::columnExists('users', 'push_token')) { DB::run('UPDATE users SET push_token=? WHERE id=?', [$token, $userId]); } } function deactivateDeviceToken(int $userId, string $token): void { $token = trim($token); if ($token === '') return; if (hasDeviceTokensTable()) { DB::run('UPDATE device_tokens SET is_active=0, last_seen_at=NOW() WHERE user_id=? AND push_token=?', [$userId, $token]); } if (DB::columnExists('users', 'push_token')) { $current = DB::val('SELECT push_token FROM users WHERE id=?', [$userId]); if ((string)$current === $token) { DB::run('UPDATE users SET push_token=NULL WHERE id=?', [$userId]); } } } function hasNotificationsTable(): bool { return DB::tableExists('notifications'); } function hasPetCareRemindersTable(): bool { return DB::tableExists('pet_care_reminders'); } function queueNotification( int $userId, string $type, string $title, string $body, array $meta = [] ): void { if (!hasNotificationsTable()) return; DB::run( 'INSERT INTO notifications (user_id, pet_id, clinic_id, appointment_id, reminder_id, type, title, body, channel, audience, status, data_json, scheduled_for, sent_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)', [ $userId, $meta['pet_id'] ?? null, $meta['clinic_id'] ?? null, $meta['appointment_id'] ?? null, $meta['reminder_id'] ?? null, $type, $title, $body, $meta['channel'] ?? 'in_app', $meta['audience'] ?? 'transactional', $meta['status'] ?? 'queued', !empty($meta['data']) ? json_encode($meta['data'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : null, $meta['scheduled_for'] ?? null, $meta['sent_at'] ?? null, ] ); } // Route $uri = rtrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/'); $uri = preg_replace('#^/api#', '', $uri); $method = $_SERVER['REQUEST_METHOD']; try { switch ("$method $uri") { case 'GET /meta/bootstrap': { $manifestPath = ROOT . '/manifest.webmanifest'; $swPath = ROOT . '/sw.js'; api([ 'ok' => true, 'app' => [ 'name' => defined('APP_NAME') ? APP_NAME : 'VETTA', 'url' => defined('APP_URL') ? APP_URL : '', 'api_base' => (defined('APP_URL') ? rtrim(APP_URL, '/') : '') . '/api', 'environment' => defined('APP_ENV') ? APP_ENV : 'production', ], 'mobile' => [ 'pwa_enabled' => file_exists($manifestPath) && file_exists($swPath), 'native_wrapper_ready' => false, 'android_apk_available' => false, 'ios_app_available' => false, 'device_tokens_ready' => hasDeviceTokensTable(), 'notification_preferences_ready' => hasNotificationPrefsTable(), 'notifications_ready' => DB::tableExists('notifications'), ], 'assets' => [ 'manifest' => file_exists($manifestPath) ? '/manifest.webmanifest' : null, 'service_worker' => file_exists($swPath) ? '/sw.js' : null, 'install_page' => '/install', ], ]); } // ── AUTH ──────────────────────────────────────────────── case 'POST /auth/login': { $b = body(); $user = DB::row('SELECT * FROM users WHERE email=?', [trim($b['email']??'')]); if (!$user || !password_verify($b['password']??'', $user->password)) api(['ok'=>false,'msg'=>'Невалиден email или парола.'], 401); if ($user->is_banned) api(['ok'=>false,'msg'=>'Акаунтът е блокиран.'], 403); // Save push token if provided if (!empty($b['push_token'])) { saveDeviceToken( (int)$user->id, (string)$b['push_token'], (string)($b['platform'] ?? 'web'), isset($b['app_version']) ? (string)$b['app_version'] : null, isset($b['locale']) ? (string)$b['locale'] : null ); } $token = JWT::encode(['uid'=>$user->id,'role'=>$user->role]); api(['ok'=>true,'token'=>$token,'user'=>[ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'role' => $user->role, 'photo_url' => $user->photo_url, 'premium_until' => $user->premium_until, 'is_premium' => $user->premium_until && $user->premium_until >= date('Y-m-d'), ]]); } case 'POST /auth/register': { $b = body(); $name = trim($b['name']??''); $email= trim($b['email']??''); $pass = $b['password']??''; if (!$name||!$email||!$pass) api(['ok'=>false,'msg'=>'Попълнете всички полета.']); if (!filter_var($email,FILTER_VALIDATE_EMAIL)) api(['ok'=>false,'msg'=>'Невалиден email.']); if (strlen($pass)<8) api(['ok'=>false,'msg'=>'Паролата трябва да е поне 8 знака.']); if (DB::row('SELECT id FROM users WHERE email=?',[$email])) api(['ok'=>false,'msg'=>'Email вече съществува.']); $hash = password_hash($pass, PASSWORD_BCRYPT); DB::run('INSERT INTO users (name,email,password,role) VALUES (?,?,?,?)',[$name,$email,$hash,'owner']); $uid = DB::lastId(); $token= JWT::encode(['uid'=>$uid,'role'=>'owner']); api(['ok'=>true,'token'=>$token,'user'=>['id'=>$uid,'name'=>$name,'email'=>$email,'role'=>'owner','is_premium'=>false]]); } case 'GET /auth/me': { $user = apiAuth(); api(['ok'=>true,'user'=>[ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'role' => $user->role, 'phone' => $user->phone, 'photo_url' => $user->photo_url, 'premium_until' => $user->premium_until, 'is_premium' => $user->premium_until && $user->premium_until >= date('Y-m-d'), ]]); } case 'GET /notifications/preferences': { $user = apiAuth(); if (!hasNotificationPrefsTable()) { api([ 'ok' => true, 'preferences' => [ 'appointment_push' => 1, 'chat_push' => 1, 'medical_push' => 1, 'deworming_push' => 1, 'vaccine_push' => 1, 'promo_push' => 0, 'email_enabled' => 1, 'quiet_hours_from' => null, 'quiet_hours_to' => null, ], 'storage' => 'fallback', ]); } $prefs = DB::row('SELECT * FROM notification_preferences WHERE user_id=?', [$user->id]); if (!$prefs) { DB::run('INSERT INTO notification_preferences (user_id) VALUES (?)', [$user->id]); $prefs = DB::row('SELECT * FROM notification_preferences WHERE user_id=?', [$user->id]); } api(['ok' => true, 'preferences' => $prefs, 'storage' => 'table']); } case 'POST /notifications/preferences': { $user = apiAuth(); $b = body(); if (!hasNotificationPrefsTable()) { api(['ok' => false, 'msg' => 'Notification preferences table is missing. Run the notifications migration first.'], 409); } $fields = [ 'appointment_push', 'chat_push', 'medical_push', 'deworming_push', 'vaccine_push', 'promo_push', 'email_enabled', ]; $values = []; foreach ($fields as $field) { $values[$field] = isset($b[$field]) ? (int)!!$b[$field] : 0; } $quietFrom = trim((string)($b['quiet_hours_from'] ?? '')); $quietTo = trim((string)($b['quiet_hours_to'] ?? '')); $quietFrom = $quietFrom !== '' ? $quietFrom : null; $quietTo = $quietTo !== '' ? $quietTo : null; DB::run( 'INSERT INTO notification_preferences (user_id, appointment_push, chat_push, medical_push, deworming_push, vaccine_push, promo_push, email_enabled, quiet_hours_from, quiet_hours_to) VALUES (?,?,?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE appointment_push=VALUES(appointment_push), chat_push=VALUES(chat_push), medical_push=VALUES(medical_push), deworming_push=VALUES(deworming_push), vaccine_push=VALUES(vaccine_push), promo_push=VALUES(promo_push), email_enabled=VALUES(email_enabled), quiet_hours_from=VALUES(quiet_hours_from), quiet_hours_to=VALUES(quiet_hours_to)', [ $user->id, $values['appointment_push'], $values['chat_push'], $values['medical_push'], $values['deworming_push'], $values['vaccine_push'], $values['promo_push'], $values['email_enabled'], $quietFrom, $quietTo, ] ); $prefs = DB::row('SELECT * FROM notification_preferences WHERE user_id=?', [$user->id]); api(['ok' => true, 'preferences' => $prefs]); } case 'GET /devices': { $user = apiAuth(); if (!hasDeviceTokensTable()) { api(['ok' => true, 'devices' => [], 'storage' => 'fallback']); } $devices = DB::all( 'SELECT id, platform, push_token, app_version, locale, is_active, last_seen_at, created_at FROM device_tokens WHERE user_id=? ORDER BY is_active DESC, last_seen_at DESC, created_at DESC', [$user->id] ); api(['ok' => true, 'devices' => $devices, 'storage' => 'table']); } case 'POST /devices/register': { $user = apiAuth(); $b = body(); $token = trim((string)($b['token'] ?? $b['push_token'] ?? '')); if ($token === '') api(['ok' => false, 'msg' => 'Missing device token.'], 422); saveDeviceToken( (int)$user->id, $token, (string)($b['platform'] ?? 'web'), isset($b['app_version']) ? (string)$b['app_version'] : null, isset($b['locale']) ? (string)$b['locale'] : null ); api(['ok' => true, 'msg' => 'Device token saved.']); } case 'POST /devices/deactivate': { $user = apiAuth(); $b = body(); $token = trim((string)($b['token'] ?? $b['push_token'] ?? '')); if ($token === '') api(['ok' => false, 'msg' => 'Missing device token.'], 422); deactivateDeviceToken((int)$user->id, $token); api(['ok' => true, 'msg' => 'Device token deactivated.']); } case 'GET /notifications': { $user = apiAuth(); if (!hasNotificationsTable()) { api(['ok' => true, 'notifications' => [], 'storage' => 'fallback']); } $limit = max(1, min(100, (int)($_GET['limit'] ?? 30))); $status = trim((string)($_GET['status'] ?? '')); $params = [$user->id]; $sql = 'SELECT * FROM notifications WHERE user_id=?'; if ($status !== '') { if ($status === 'unread') { $sql .= ' AND status<>"read"'; } else { $sql .= ' AND status=?'; $params[] = $status; } } $sql .= ' ORDER BY COALESCE(scheduled_for, created_at) DESC, id DESC LIMIT ' . $limit; $notifications = DB::all($sql, $params); api(['ok' => true, 'notifications' => $notifications, 'storage' => 'table']); } case 'GET /notifications/unread': { $user = apiAuth(); if (!hasNotificationsTable()) { api(['ok' => true, 'count' => 0, 'storage' => 'fallback']); } $count = (int)DB::val('SELECT COUNT(*) FROM notifications WHERE user_id=? AND status<>"read"', [$user->id]); api(['ok' => true, 'count' => $count, 'storage' => 'table']); } case 'POST /notifications/read': { $user = apiAuth(); $b = body(); if (!hasNotificationsTable()) { api(['ok' => true, 'msg' => 'Notifications table missing, nothing to mark.']); } $id = (int)($b['id'] ?? 0); if ($id > 0) { DB::run('UPDATE notifications SET status="read", read_at=NOW() WHERE id=? AND user_id=?', [$id, $user->id]); } else { DB::run('UPDATE notifications SET status="read", read_at=NOW() WHERE user_id=? AND status<>"read"', [$user->id]); } api(['ok' => true]); } case 'GET /reminders': { $user = apiAuth(); if (!hasPetCareRemindersTable()) { api(['ok' => true, 'reminders' => [], 'storage' => 'fallback']); } $status = trim((string)($_GET['status'] ?? 'pending')); $petId = (int)($_GET['pet_id'] ?? 0); $params = [$user->id]; $sql = 'SELECT * FROM pet_care_reminders WHERE owner_id=?'; if ($status !== '') { $sql .= ' AND status=?'; $params[] = $status; } if ($petId > 0) { $sql .= ' AND pet_id=?'; $params[] = $petId; } $sql .= ' ORDER BY due_date ASC, id DESC'; api(['ok' => true, 'reminders' => DB::all($sql, $params), 'storage' => 'table']); } case 'POST /reminders': { $user = apiAuth(); $b = body(); if (!hasPetCareRemindersTable()) { api(['ok' => false, 'msg' => 'Reminders table is missing. Run the notifications migration first.'], 409); } $petId = (int)($b['pet_id'] ?? 0); $pet = DB::row('SELECT * FROM pets WHERE id=? AND owner_id=?', [$petId, $user->id]); if (!$pet) api(['ok' => false, 'msg' => 'Невалидно животно.'], 404); $title = trim((string)($b['title'] ?? '')); $dueDate = trim((string)($b['due_date'] ?? '')); if ($title === '' || $dueDate === '') api(['ok' => false, 'msg' => 'Липсва заглавие или дата.'], 422); DB::run( 'INSERT INTO pet_care_reminders (pet_id, owner_id, clinic_id, vet_user_id, reminder_type, title, description, due_date, repeat_interval_days, created_by) VALUES (?,?,?,?,?,?,?,?,?,?)', [ $petId, $user->id, isset($b['clinic_id']) ? (int)$b['clinic_id'] : null, isset($b['vet_user_id']) ? (int)$b['vet_user_id'] : null, (string)($b['reminder_type'] ?? 'custom'), $title, (string)($b['description'] ?? ''), $dueDate, isset($b['repeat_interval_days']) && (int)$b['repeat_interval_days'] > 0 ? (int)$b['repeat_interval_days'] : null, $user->id, ] ); $reminderId = (int)DB::lastId(); queueNotification( (int)$user->id, 'reminder_created', 'Ново напомняне', 'Създадохме напомняне за ' . $pet->name . ': ' . $title, [ 'pet_id' => $petId, 'reminder_id' => $reminderId, 'data' => ['due_date' => $dueDate], ] ); api(['ok' => true, 'id' => $reminderId]); } case 'POST /reminders/complete': { $user = apiAuth(); $b = body(); if (!hasPetCareRemindersTable()) { api(['ok' => false, 'msg' => 'Reminders table is missing.'], 409); } $id = (int)($b['id'] ?? 0); $reminder = DB::row('SELECT * FROM pet_care_reminders WHERE id=? AND owner_id=?', [$id, $user->id]); if (!$reminder) api(['ok' => false, 'msg' => 'Не е намерено напомняне.'], 404); DB::run('UPDATE pet_care_reminders SET status="completed", last_completed_at=CURDATE() WHERE id=?', [$id]); if (!empty($reminder->repeat_interval_days) && (int)$reminder->repeat_interval_days > 0) { $nextDate = date('Y-m-d', strtotime($reminder->due_date . ' +' . (int)$reminder->repeat_interval_days . ' days')); DB::run( 'INSERT INTO pet_care_reminders (pet_id, owner_id, clinic_id, vet_user_id, reminder_type, title, description, due_date, repeat_interval_days, created_by) VALUES (?,?,?,?,?,?,?,?,?,?)', [ $reminder->pet_id, $reminder->owner_id, $reminder->clinic_id, $reminder->vet_user_id, $reminder->reminder_type, $reminder->title, $reminder->description, $nextDate, $reminder->repeat_interval_days, $reminder->created_by, ] ); } api(['ok' => true]); } case 'POST /reminders/cancel': { $user = apiAuth(); $b = body(); if (!hasPetCareRemindersTable()) { api(['ok' => false, 'msg' => 'Reminders table is missing.'], 409); } $id = (int)($b['id'] ?? 0); DB::run('UPDATE pet_care_reminders SET status="cancelled" WHERE id=? AND owner_id=?', [$id, $user->id]); api(['ok' => true]); } // ── PETS ───────────────────────────────────────────────── case 'GET /pets': { $user = apiAuth(); $pets = DB::all('SELECT * FROM pets WHERE owner_id=? ORDER BY name', [$user->id]); api(['ok'=>true,'pets'=>$pets]); } case 'POST /pets': { $user = apiAuth(); $b = body(); $name = trim($b['name']??''); if (!$name) api(['ok'=>false,'msg'=>'Въведете кличка.']); $is_premium = $user->premium_until && $user->premium_until >= date('Y-m-d'); if (!$is_premium) { $cnt = (int)DB::val('SELECT COUNT(*) FROM pets WHERE owner_id=?',[$user->id]); if ($cnt >= 1) api(['ok'=>false,'msg'=>'Безплатният план позволява само 1 животно.']); } DB::run('INSERT INTO pets (owner_id,name,species,breed,gender,birthdate,color,chip_id,notes) VALUES (?,?,?,?,?,?,?,?,?)', [$user->id,$name,$b['species']??'other',$b['breed']??'',$b['gender']??null, $b['birthdate']??null,$b['color']??'',$b['chip_id']??'',$b['notes']??'']); api(['ok'=>true,'id'=>DB::lastId(),'msg'=>'Животното е добавено!']); } case 'DELETE /pets': { $user = apiAuth(); $pid = (int)($_GET['id']??0); $pet = DB::row('SELECT * FROM pets WHERE id=? AND owner_id=?',[$pid,$user->id]); if (!$pet) api(['ok'=>false,'msg'=>'Не е намерено.'],404); DB::run('DELETE FROM pets WHERE id=?',[$pid]); api(['ok'=>true]); } // ── RECORDS ────────────────────────────────────────────── case 'GET /records': { $user = apiAuth(); $pid = (int)($_GET['pet_id']??0); $pet = DB::row('SELECT * FROM pets WHERE id=? AND owner_id=?',[$pid,$user->id]); if (!$pet) api(['ok'=>false,'msg'=>'Не е намерено.'],404); $is_premium = $user->premium_until && $user->premium_until >= date('Y-m-d'); $limit = $is_premium ? 999 : 5; $recs = DB::all('SELECT mr.*,u.name as vet_name,c.name as clinic_name FROM medical_records mr LEFT JOIN users u ON u.id=mr.vet_user_id LEFT JOIN clinics c ON c.id=mr.clinic_id WHERE mr.pet_id=? ORDER BY mr.visit_date DESC LIMIT ?',[$pid,$limit]); api(['ok'=>true,'records'=>$recs,'pet'=>$pet]); } case 'POST /records': { $user = apiAuth(); $b = body(); $pid = (int)($b['pet_id']??0); // Must be vet or clinic_admin if (!in_array($user->role,['vet','clinic_admin'])) api(['ok'=>false,'msg'=>'Само ветеринари могат да добавят записи.'],403); $cu = DB::row('SELECT * FROM clinic_users WHERE user_id=?',[$user->id]); if (!$cu) api(['ok'=>false,'msg'=>'Нямате клиника.'],403); $ok = DB::row('SELECT ag.id FROM access_grants ag JOIN pets p ON p.id=ag.pet_id WHERE ag.pet_id=? AND ag.clinic_id=?',[$pid,$cu->clinic_id]); if (!$ok) api(['ok'=>false,'msg'=>'Нямате достъп до това животно.'],403); DB::run('INSERT INTO medical_records (pet_id,clinic_id,vet_user_id,record_type,visit_date,symptoms,diagnosis,treatment,weight,temperature,pulse,next_visit_date) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [$pid,$cu->clinic_id,$user->id, $b['record_type']??'checkup', $b['visit_date']??date('Y-m-d H:i:s'), $b['symptoms']??'',$b['diagnosis']??'',$b['treatment']??'', $b['weight']??null,$b['temperature']??null,$b['pulse']??null, $b['next_visit']??null]); api(['ok'=>true,'id'=>DB::lastId()]); } // ── CLINICS ────────────────────────────────────────────── case 'GET /clinics': { $user = apiAuth(); if ($user->role === 'owner') { // My approved clinics $clinics = DB::all('SELECT DISTINCT c.*,1 as granted FROM clinics c JOIN access_grants ag ON ag.clinic_id=c.id JOIN pets p ON p.id=ag.pet_id WHERE p.owner_id=? AND c.status="active"', [$user->id]); } else { $cu = DB::row('SELECT * FROM clinic_users WHERE user_id=?',[$user->id]); $clinics = $cu ? [DB::row('SELECT * FROM clinics WHERE id=?',[$cu->clinic_id])] : []; } api(['ok'=>true,'clinics'=>array_values(array_filter($clinics))]); } case 'GET /clinics/search': { apiAuth(); $city = trim($_GET['city']??''); $q = trim($_GET['q']??''); $where= ['c.status="active"']; $params=[]; if ($city) { $where[]='c.city=?'; $params[]=$city; } if ($q) { $where[]='(c.name LIKE ? OR c.city LIKE ?)'; $params[]="%$q%"; $params[]="%$q%"; } $clinics = DB::all('SELECT c.*,(SELECT COUNT(*) FROM clinic_users cu WHERE cu.clinic_id=c.id) as team_count FROM clinics c WHERE '.implode(' AND ',$where).' ORDER BY c.name',$params); $cities = DB::all('SELECT DISTINCT city FROM clinics WHERE status="active" AND city IS NOT NULL AND city!="" ORDER BY city'); api(['ok'=>true,'clinics'=>$clinics,'cities'=>array_column($cities,'city')]); } case 'POST /clinics/request': { $user = apiAuth(); $b = body(); $pid = (int)($b['pet_id']??0); $cid = (int)($b['clinic_id']??0); $pet = DB::row('SELECT * FROM pets WHERE id=? AND owner_id=?',[$pid,$user->id]); if (!$pet) api(['ok'=>false,'msg'=>'Невалидно животно.']); if (DB::row('SELECT id FROM access_grants WHERE pet_id=? AND clinic_id=?',[$pid,$cid])) api(['ok'=>false,'msg'=>'Вече имате достъп.']); if (DB::row('SELECT id FROM access_requests WHERE pet_id=? AND clinic_id=? AND status="pending"',[$pid,$cid])) api(['ok'=>false,'msg'=>'Вече има чакаща заявка.']); DB::run('INSERT INTO access_requests (pet_id,owner_id,clinic_id) VALUES (?,?,?)',[$pid,$user->id,$cid]); api(['ok'=>true,'msg'=>'Заявката е изпратена!']); } // ── APPOINTMENTS ───────────────────────────────────────── case 'GET /appointments': { $user = apiAuth(); if ($user->role === 'owner') { $appts = DB::all('SELECT a.*,p.name as pet_name,c.name as clinic_name FROM appointments a JOIN pets p ON p.id=a.pet_id JOIN clinics c ON c.id=a.clinic_id WHERE a.owner_id=? ORDER BY a.appt_date DESC LIMIT 20',[$user->id]); } else { $cu = DB::row('SELECT * FROM clinic_users WHERE user_id=?',[$user->id]); $appts = $cu ? DB::all('SELECT a.*,p.name as pet_name,u.name as owner_name FROM appointments a JOIN pets p ON p.id=a.pet_id JOIN users u ON u.id=a.owner_id WHERE a.clinic_id=? AND a.appt_date>=CURDATE() ORDER BY a.appt_date LIMIT 30',[$cu->clinic_id]) : []; } api(['ok'=>true,'appointments'=>$appts]); } case 'GET /appointments/slots': { apiAuth(); $cid = (int)($_GET['clinic_id']??0); $date = $_GET['date']??date('Y-m-d'); $slots= DB::all('SELECT * FROM clinic_slots WHERE clinic_id=? AND slot_date=? AND is_taken=0 ORDER BY slot_time',[$cid,$date]); api(['ok'=>true,'slots'=>$slots]); } case 'POST /appointments/book': { $user = apiAuth(); $b = body(); $is_premium = $user->premium_until && $user->premium_until >= date('Y-m-d'); if (!$is_premium) api(['ok'=>false,'msg'=>'Онлайн записването е само за Premium.']); $slot = DB::row('SELECT * FROM clinic_slots WHERE id=? AND is_taken=0',[(int)($b['slot_id']??0)]); if (!$slot) api(['ok'=>false,'msg'=>'Часът не е наличен.']); DB::run('INSERT INTO appointments (clinic_id,pet_id,owner_id,title,notes,appt_date,status) VALUES (?,?,?,?,?,?,?)', [$slot->clinic_id,$b['pet_id']??0,$user->id,$b['title']??'Час',$b['notes']??'',$slot->slot_date.' '.$slot->slot_time,'pending']); $aid = DB::lastId(); DB::run('UPDATE clinic_slots SET is_taken=1,appt_id=? WHERE id=?',[$aid,$slot->id]); $clinic = DB::row('SELECT name FROM clinics WHERE id=?', [$slot->clinic_id]); queueNotification( (int)$user->id, 'appointment_booked', 'Часът е записан', 'Записа час в ' . ($clinic->name ?? 'клиниката') . ' за ' . date('d.m.Y H:i', strtotime($slot->slot_date . ' ' . $slot->slot_time)), [ 'pet_id' => (int)($b['pet_id'] ?? 0), 'clinic_id' => (int)$slot->clinic_id, 'appointment_id' => (int)$aid, 'data' => ['status' => 'pending'], ] ); api(['ok'=>true,'id'=>$aid]); } // ── CHAT ───────────────────────────────────────────────── case 'GET /chat/conversations': { $user = apiAuth(); if ($user->role === 'owner') { $convs = DB::all('SELECT c.id,c.name,c.photo_url, (SELECT body FROM messages WHERE clinic_id=c.id AND owner_id=? ORDER BY created_at DESC LIMIT 1) as last_msg, (SELECT COUNT(*) FROM messages WHERE clinic_id=c.id AND owner_id=? AND sender_id!=? AND is_read=0) as unread FROM clinics c JOIN access_grants ag ON ag.clinic_id=c.id JOIN pets p ON p.id=ag.pet_id WHERE p.owner_id=? GROUP BY c.id ORDER BY c.name', [$user->id,$user->id,$user->id,$user->id]); } else { $cu = DB::row('SELECT * FROM clinic_users WHERE user_id=?',[$user->id]); if (!$cu) api(['ok'=>true,'conversations'=>[]]); $convs = DB::all('SELECT DISTINCT u.id as owner_id,u.name as owner_name,u.photo_url, (SELECT body FROM messages WHERE clinic_id=? AND owner_id=u.id ORDER BY created_at DESC LIMIT 1) as last_msg, (SELECT COUNT(*) FROM messages WHERE clinic_id=? AND owner_id=u.id AND sender_id!=? AND is_read=0) as unread FROM messages m JOIN users u ON u.id=m.owner_id WHERE m.clinic_id=? ORDER BY m.created_at DESC', [$cu->clinic_id,$cu->clinic_id,$user->id,$cu->clinic_id]); } api(['ok'=>true,'conversations'=>$convs]); } case 'GET /chat/messages': { $user = apiAuth(); $cid = (int)($_GET['clinic_id']??0); $oid = $user->role==='owner' ? $user->id : (int)($_GET['owner_id']??0); $msgs = DB::all('SELECT m.*,u.name as sender_name FROM messages m JOIN users u ON u.id=m.sender_id WHERE m.clinic_id=? AND m.owner_id=? ORDER BY m.created_at ASC LIMIT 100',[$cid,$oid]); // Mark as read DB::run('UPDATE messages SET is_read=1 WHERE clinic_id=? AND owner_id=? AND sender_id!=?',[$cid,$oid,$user->id]); api(['ok'=>true,'messages'=>$msgs]); } case 'POST /chat/send': { $user = apiAuth(); $b = body(); $cid = (int)($b['clinic_id']??0); $oid = $user->role==='owner' ? $user->id : (int)($b['owner_id']??0); $msg = trim($b['message']??''); if (!$msg) api(['ok'=>false,'msg'=>'Съобщението е празно.']); DB::run('INSERT INTO messages (clinic_id,owner_id,sender_id,body) VALUES (?,?,?,?)',[$cid,$oid,$user->id,$msg]); if ($user->role === 'owner') { $staff = DB::all('SELECT user_id FROM clinic_users WHERE clinic_id=?', [$cid]); foreach ($staff as $member) { queueNotification( (int)$member->user_id, 'chat_message', 'Ново съобщение от клиент', mb_strimwidth($msg, 0, 120, '...'), [ 'clinic_id' => $cid, 'data' => ['owner_id' => $oid], ] ); } } else { queueNotification( (int)$oid, 'chat_message', 'Ново съобщение от клиника', mb_strimwidth($msg, 0, 120, '...'), [ 'clinic_id' => $cid, 'data' => ['owner_id' => $oid], ] ); } api(['ok'=>true,'id'=>DB::lastId()]); } // ── PATIENTS (for clinic/vet) ───────────────────────────── case 'GET /patients': { $user = apiAuth(); if (!in_array($user->role,['vet','clinic_admin'])) api(['ok'=>false,'msg'=>'Forbidden'],403); $cu = DB::row('SELECT * FROM clinic_users WHERE user_id=?',[$user->id]); if (!$cu) api(['ok'=>true,'patients'=>[]]); $q = trim($_GET['q']??''); $sql = 'SELECT p.*,u.name as owner_name,u.phone as owner_phone,COUNT(mr.id) as record_count FROM pets p JOIN access_grants ag ON ag.pet_id=p.id AND ag.clinic_id=? JOIN users u ON u.id=p.owner_id LEFT JOIN medical_records mr ON mr.pet_id=p.id '.($q ? 'WHERE p.name LIKE ? OR u.name LIKE ?' : '').' GROUP BY p.id ORDER BY p.name LIMIT 50'; $params = $q ? [$cu->clinic_id,"%$q%","%$q%"] : [$cu->clinic_id]; api(['ok'=>true,'patients'=>DB::all($sql,$params),'clinic'=>DB::row('SELECT * FROM clinics WHERE id=?',[$cu->clinic_id])]); } // ── REQUESTS (for clinic) ───────────────────────────────── case 'GET /requests': { $user = apiAuth(); if (!in_array($user->role,['vet','clinic_admin'])) api(['ok'=>false,'msg'=>'Forbidden'],403); $cu = DB::row('SELECT * FROM clinic_users WHERE user_id=?',[$user->id]); if (!$cu) api(['ok'=>true,'requests'=>[]]); $reqs = DB::all('SELECT ar.*,p.name as pet_name,p.species as pet_species,u.name as owner_name,u.phone as owner_phone FROM access_requests ar JOIN pets p ON p.id=ar.pet_id JOIN users u ON u.id=ar.owner_id WHERE ar.clinic_id=? AND ar.status="pending" ORDER BY ar.created_at DESC',[$cu->clinic_id]); api(['ok'=>true,'requests'=>$reqs]); } case 'POST /requests/approve': { $user = apiAuth(); if (!in_array($user->role,['vet','clinic_admin'])) api(['ok'=>false,'msg'=>'Forbidden'],403); $b = body(); $rid = (int)($b['req_id']??0); $cu = DB::row('SELECT * FROM clinic_users WHERE user_id=?',[$user->id]); $req = DB::row('SELECT * FROM access_requests WHERE id=? AND clinic_id=?',[$rid,$cu->clinic_id??0]); if (!$req) api(['ok'=>false,'msg'=>'Не е намерено.'],404); DB::run('UPDATE access_requests SET status="approved" WHERE id=?',[$rid]); if (!DB::row('SELECT id FROM access_grants WHERE pet_id=? AND clinic_id=?',[$req->pet_id,$req->clinic_id])) DB::run('INSERT INTO access_grants (pet_id,clinic_id) VALUES (?,?)',[$req->pet_id,$req->clinic_id]); $clinic = DB::row('SELECT name FROM clinics WHERE id=?', [$req->clinic_id]); queueNotification( (int)$req->owner_id, 'access_request_approved', 'Заявката е одобрена', 'Клиника ' . ($clinic->name ?? '') . ' одобри достъпа до любимеца ти.', [ 'pet_id' => (int)$req->pet_id, 'clinic_id' => (int)$req->clinic_id, ] ); api(['ok'=>true]); } case 'POST /requests/reject': { $user = apiAuth(); $b = body(); $cu = DB::row('SELECT * FROM clinic_users WHERE user_id=?',[$user->id]); $req = DB::row('SELECT * FROM access_requests WHERE id=? AND clinic_id=?',[(int)($b['req_id']??0),$cu->clinic_id??0]); if (!$req) api(['ok'=>false,'msg'=>'Не е намерено.'],404); DB::run('UPDATE access_requests SET status="rejected" WHERE id=?',[$req->id]); $clinic = DB::row('SELECT name FROM clinics WHERE id=?', [$req->clinic_id]); queueNotification( (int)$req->owner_id, 'access_request_rejected', 'Заявката е отказана', 'Клиника ' . ($clinic->name ?? '') . ' отказа заявката за достъп.', [ 'pet_id' => (int)$req->pet_id, 'clinic_id' => (int)$req->clinic_id, ] ); api(['ok'=>true]); } // ── CARDS ACTIVATE ──────────────────────────────────────── case 'POST /cards/activate': { $user = apiAuth(); $b = body(); $code = strtoupper(trim($b['code']??'')); $card = DB::row('SELECT * FROM cards WHERE code=? AND status="unused"',[$code]); if (!$card) api(['ok'=>false,'msg'=>'Невалиден или използван код.']); $months = (int)$card->months; $base = $user->premium_until && $user->premium_until > date('Y-m-d') ? $user->premium_until : date('Y-m-d'); $until = date('Y-m-d', strtotime($base." +$months months")); DB::run('UPDATE users SET premium_until=? WHERE id=?',[$until,$user->id]); DB::run('UPDATE cards SET status="used",used_by=?,used_at=NOW() WHERE id=?',[$user->id,$card->id]); api(['ok'=>true,'premium_until'=>$until,'msg'=>'Premium активиран до '.$until.'!']); } // ── STATS (for admin dashboard widget) ─────────────────── case 'GET /stats': { $user = apiAuth(); if ($user->role !== 'super_admin') api(['ok'=>false,'msg'=>'Forbidden'],403); api(['ok'=>true,'stats'=>[ 'users' => (int)DB::val('SELECT COUNT(*) FROM users'), 'pets' => (int)DB::val('SELECT COUNT(*) FROM pets'), 'clinics' => (int)DB::val('SELECT COUNT(*) FROM clinics'), 'premium' => (int)DB::val('SELECT COUNT(*) FROM users WHERE premium_until>=CURDATE()'), ]]); } // ── PUSH TOKEN ──────────────────────────────────────────── case 'POST /push-token': { $user = apiAuth(); $b = body(); $token = trim((string)($b['token'] ?? $b['push_token'] ?? '')); if ($token !== '') { saveDeviceToken( (int)$user->id, $token, (string)($b['platform'] ?? 'web'), isset($b['app_version']) ? (string)$b['app_version'] : null, isset($b['locale']) ? (string)$b['locale'] : null ); } api(['ok'=>true,'msg'=>'Push token saved.']); } default: api(['ok'=>false,'msg'=>'Not found'], 404); } } catch (Throwable $ex) { api(['ok'=>false,'msg'=>$ex->getMessage(),'file'=>APP_ENV==='development'?basename($ex->getFile()).':'.$ex->getLine():null], 500); }
| ver. 1.4 |
Github
|
.
| PHP 8.2.31 | Generation time: 0.07 |
proxy
|
phpinfo
|
Settings