Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b19e996e1e | ||
|
|
83aa809d57 | ||
|
|
d22f5a1d5d | ||
|
|
69846c8387 | ||
|
|
8f9c7c6314 | ||
|
|
e09626b316 | ||
|
|
e150954bb6 | ||
|
|
5eaa8cc290 | ||
|
|
9667cec7e6 | ||
|
|
3ca5cde9e3 | ||
|
|
620485b60a | ||
|
|
fbacfd6e29 | ||
|
|
0690a23bb7 | ||
|
|
5f99108a0d | ||
|
|
132df22fd4 | ||
|
|
97ae4ba490 | ||
|
|
329da3e5d5 | ||
|
|
a03fa77672 | ||
|
|
04bd70e7d8 | ||
|
|
bcd593fb90 | ||
|
|
e73384ddbc | ||
|
|
0d5768a3f8 | ||
|
|
4472d09232 | ||
|
|
6933c13c3e | ||
|
|
f62878214d | ||
|
|
10338c6bc6 | ||
|
|
b02c277bb8 | ||
|
|
5f98198b67 | ||
|
|
f10cdedce9 | ||
|
|
70cf52dd48 | ||
|
|
07e733df85 | ||
|
|
23c927161f | ||
|
|
3912cd5bd2 | ||
|
|
6387f7c226 | ||
|
|
5e776c9e2b | ||
|
|
68751660aa | ||
|
|
d96399430d | ||
|
|
66038df9e4 | ||
|
|
1cb1f89067 | ||
|
|
a6e75b0397 | ||
|
|
2bc76dec94 | ||
|
|
dc081ef86a | ||
|
|
b5ea341aa5 | ||
|
|
dad73767a1 | ||
|
|
fd41c8937a | ||
|
|
bab61e3570 | ||
|
|
641316f659 | ||
|
|
01f56287c6 | ||
|
|
43f495d39b | ||
|
|
9a2dea5596 | ||
|
|
2575e8e91e | ||
|
|
e4e7b4a246 | ||
|
|
adc8aae5d0 | ||
|
|
4f4d8efa33 | ||
|
|
4ea57c7524 | ||
|
|
5420bde76a | ||
|
|
4f1232c067 | ||
|
|
5f78c2abad | ||
|
|
0a3eff3c92 | ||
|
|
d79aa1e948 | ||
|
|
523db02f6f | ||
|
|
230a62f266 | ||
|
|
2b7bb7957e | ||
|
|
6439167fd2 | ||
|
|
48924b82af | ||
|
|
4d100ba658 | ||
|
|
d04323d02a | ||
|
|
e15aadbaf9 | ||
|
|
defcfeb7da | ||
|
|
c3784f3565 | ||
|
|
c8679068a9 | ||
|
|
c6904a9a5e | ||
|
|
d3172b6c89 | ||
|
|
6f43ba8047 | ||
|
|
933409e6cf | ||
|
|
a63d31ec70 | ||
|
|
63a866d898 | ||
|
|
85f641cde7 | ||
|
|
aa6a7b2ed1 |
13
.gemini/GEMINI_LAST_MESSAGE.txt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
Plan:
|
||||||
|
1. **Investigate:** Checked Apache error logs and `includes/actions.php`.
|
||||||
|
2. **Identify Issue:** Found a mismatch between the database schema for `insurance_payments` (missing `payment_method` column) and the PHP code trying to insert into it.
|
||||||
|
3. **Fix:** Created and applied a migration to add the `payment_method` column to the `insurance_payments` table.
|
||||||
|
4. **Verify:** Confirmed the column now exists in the database.
|
||||||
|
|
||||||
|
Changed Files:
|
||||||
|
* `db/migrations/20260321_z_add_payment_method_to_insurance_payments.sql`: Added migration to fix the schema.
|
||||||
|
|
||||||
|
Next Steps:
|
||||||
|
* Go to **Insurance > Transactions**.
|
||||||
|
* Try adding a transaction again. It should work now.
|
||||||
|
* **Reminder:** Click Save in the editor to sync changes.
|
||||||
113
SETUP.md
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
# Project Setup Guide
|
||||||
|
|
||||||
|
This guide provides step-by-step instructions to set up, configure, and run the Hospital Management System.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
* **PHP**: Version 8.0 or higher.
|
||||||
|
* **Database**: MariaDB 10.x or MySQL 5.7+.
|
||||||
|
* **Web Server**: Apache 2.4+ (with `mod_rewrite` enabled).
|
||||||
|
* **Composer**: (Optional) For dependency management if using third-party packages in the future.
|
||||||
|
|
||||||
|
## Installation Steps
|
||||||
|
|
||||||
|
1. **Clone the Repository**
|
||||||
|
```bash
|
||||||
|
git clone <repository_url>
|
||||||
|
cd <project_directory>
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Web Server**
|
||||||
|
* Point your Apache VirtualHost `DocumentRoot` to the project directory.
|
||||||
|
* Ensure the directory has appropriate permissions (e.g., `www-data` user).
|
||||||
|
* Enable `.htaccess` overrides if applicable.
|
||||||
|
|
||||||
|
3. **Database Setup**
|
||||||
|
* Create a new MySQL/MariaDB database (e.g., `hospital_db`).
|
||||||
|
* Import the database schema and seed data. You can do this by running the initialization script or importing SQL files:
|
||||||
|
* **Option A (Script):** Run `php install.php` from the command line (ensure `db/config.php` is configured first).
|
||||||
|
* **Option B (Manual):** Import files from `db/migrations/` in chronological order using a tool like phpMyAdmin or CLI.
|
||||||
|
|
||||||
|
4. **Configuration Files**
|
||||||
|
* **Database:** Edit `db/config.php` to match your database credentials:
|
||||||
|
```php
|
||||||
|
define('DB_HOST', '127.0.0.1');
|
||||||
|
define('DB_NAME', 'your_db_name');
|
||||||
|
define('DB_USER', 'your_db_user');
|
||||||
|
define('DB_PASS', 'your_db_password');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration & Environment Variables
|
||||||
|
|
||||||
|
The application uses a mix of PHP constants and environment variables for configuration.
|
||||||
|
|
||||||
|
### Email Configuration (`mail/config.php`)
|
||||||
|
The application uses PHPMailer. Configure the following environment variables (in your server config or `.env` file):
|
||||||
|
|
||||||
|
* `MAIL_TRANSPORT`: `smtp` (default)
|
||||||
|
* `SMTP_HOST`: e.g., `smtp.gmail.com`
|
||||||
|
* `SMTP_PORT`: e.g., `587`
|
||||||
|
* `SMTP_SECURE`: `tls` or `ssl`
|
||||||
|
* `SMTP_USER`: Your email address
|
||||||
|
* `SMTP_PASS`: Your email password or App Password
|
||||||
|
* `MAIL_FROM`: Sender email address
|
||||||
|
* `MAIL_FROM_NAME`: Sender name (e.g., "Hospital Admin")
|
||||||
|
|
||||||
|
### AI Integration (`ai/config.php`)
|
||||||
|
The system uses an AI proxy for features like "AI Suggestion" in visit forms and the Telegram bot.
|
||||||
|
* **Config File:** `ai/config.php`
|
||||||
|
* **Key Settings:** `base_url`, `project_uuid`.
|
||||||
|
* **Usage:** See `ai/LocalAIApi.php`.
|
||||||
|
|
||||||
|
### Telegram Integration
|
||||||
|
The system includes a Telegram bot for answering FAQs (`api/telegram_webhook.php`).
|
||||||
|
|
||||||
|
1. **Create a Bot:** Talk to `@BotFather` on Telegram to create a bot and get a **Token**.
|
||||||
|
2. **Save Token:** Insert the token into the `settings` table in your database:
|
||||||
|
```sql
|
||||||
|
INSERT INTO settings (setting_key, setting_value) VALUES ('telegram_token', 'YOUR_TELEGRAM_BOT_TOKEN');
|
||||||
|
```
|
||||||
|
3. **Set Webhook:** Configure the webhook URL for your bot:
|
||||||
|
```
|
||||||
|
https://api.telegram.org/bot<YOUR_TOKEN>/setWebhook?url=https://<YOUR_DOMAIN>/api/telegram_webhook.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integrations & Libraries
|
||||||
|
|
||||||
|
### Frontend (CDN)
|
||||||
|
The project relies on the following CDN-hosted libraries. Ensure you have an active internet connection.
|
||||||
|
|
||||||
|
* **Framework:** Bootstrap 5.3.0
|
||||||
|
* **Icons:** Bootstrap Icons 1.10.5
|
||||||
|
* **Fonts:** Google Fonts (Inter, Tajawal)
|
||||||
|
* **Forms:**
|
||||||
|
* Select2 4.1.0 (with Bootstrap 5 theme)
|
||||||
|
* Summernote Lite 0.8.18 (Rich Text Editor)
|
||||||
|
* Flatpickr (Date/Time picker)
|
||||||
|
* Inputmask 5.0.7 (Input formatting)
|
||||||
|
* **Utilities:**
|
||||||
|
* jQuery 3.6.0
|
||||||
|
* Ui-Avatars (User avatars)
|
||||||
|
* JsBarcode 3.11.5 (Patient labels)
|
||||||
|
|
||||||
|
### Backend Services
|
||||||
|
* **Email:** `mail/MailService.php` (Wraps PHPMailer).
|
||||||
|
* **AI:** `ai/LocalAIApi.php` (Handles OpenAI requests via proxy).
|
||||||
|
* **Reporting:** `includes/SimpleXLSX.php` (Excel export support).
|
||||||
|
|
||||||
|
## Default Credentials
|
||||||
|
|
||||||
|
If you used the provided migration scripts (`20260321_create_auth_system.sql`), the default administrator account is:
|
||||||
|
|
||||||
|
* **Email:** `admin@hospital.com`
|
||||||
|
* **Password:** `admin123`
|
||||||
|
* **Role:** Administrator
|
||||||
|
|
||||||
|
**Note:** Change this password immediately after logging in.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
* **Database Connection Error:** Check `db/config.php` credentials. Ensure the database server is running.
|
||||||
|
* **Email Not Sending:** Verify SMTP settings in `mail/config.php` or environment variables. Check server logs for connection timeouts.
|
||||||
|
* **AI Features Not Working:** Ensure the server can reach the AI proxy endpoint defined in `ai/config.php`.
|
||||||
|
* **Telegram Bot Not Responding:** Verify the `telegram_token` in the `settings` table and ensure the webhook URL is accessible from the public internet (HTTPS required).
|
||||||
@ -1,41 +1,63 @@
|
|||||||
<?php
|
<?php
|
||||||
// OpenAI proxy configuration (workspace scope).
|
// OpenAI proxy configuration (workspace scope).
|
||||||
// Reads values from environment variables or executor/.env.
|
// Reads values from environment variables or .env files.
|
||||||
|
|
||||||
$projectUuid = getenv('PROJECT_UUID');
|
// Helper to check env vars from multiple sources
|
||||||
$projectId = getenv('PROJECT_ID');
|
function get_env_var($key) {
|
||||||
|
$val = getenv($key);
|
||||||
|
if ($val !== false && $val !== '' && $val !== null) return $val;
|
||||||
|
if (isset($_SERVER[$key]) && $_SERVER[$key] !== '') return $_SERVER[$key];
|
||||||
|
if (isset($_ENV[$key]) && $_ENV[$key] !== '') return $_ENV[$key];
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
$projectUuid = get_env_var('PROJECT_UUID');
|
||||||
($projectUuid === false || $projectUuid === null || $projectUuid === '') ||
|
$projectId = get_env_var('PROJECT_ID');
|
||||||
($projectId === false || $projectId === null || $projectId === '')
|
|
||||||
) {
|
// If missing, try loading from .env files
|
||||||
$envPath = realpath(__DIR__ . '/../../.env'); // executor/.env
|
if (!$projectUuid || !$projectId) {
|
||||||
if ($envPath && is_readable($envPath)) {
|
// List of possible .env locations (relative to this file)
|
||||||
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
$possiblePaths = [
|
||||||
|
__DIR__ . '/../../.env', // executor/.env
|
||||||
|
__DIR__ . '/../.env', // workspace/.env
|
||||||
|
__DIR__ . '/.env', // ai/.env
|
||||||
|
dirname(__DIR__, 2) . '/.env' // Alternative root
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($possiblePaths as $path) {
|
||||||
|
$realPath = realpath($path);
|
||||||
|
if ($realPath && is_readable($realPath)) {
|
||||||
|
$lines = @file($realPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
|
||||||
foreach ($lines as $line) {
|
foreach ($lines as $line) {
|
||||||
$line = trim($line);
|
$line = trim($line);
|
||||||
if ($line === '' || $line[0] === '#') {
|
if ($line === '' || $line[0] === '#') continue;
|
||||||
continue;
|
if (!str_contains($line, '=')) continue;
|
||||||
}
|
|
||||||
if (!str_contains($line, '=')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
[$key, $value] = array_map('trim', explode('=', $line, 2));
|
||||||
if ($key === '') {
|
if ($key === '') continue;
|
||||||
continue;
|
|
||||||
}
|
$value = trim($value, "' ");
|
||||||
$value = trim($value, "\"' ");
|
// Only set if not already set
|
||||||
if (getenv($key) === false || getenv($key) === '') {
|
if (!get_env_var($key)) {
|
||||||
putenv("{$key}={$value}");
|
putenv("{$key}={$value}");
|
||||||
|
$_SERVER[$key] = $value;
|
||||||
|
$_ENV[$key] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$projectUuid = getenv('PROJECT_UUID');
|
// Stop after first successful .env load
|
||||||
$projectId = getenv('PROJECT_ID');
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$projectUuid = ($projectUuid === false) ? null : $projectUuid;
|
// Refresh vars
|
||||||
$projectId = ($projectId === false) ? null : $projectId;
|
$projectUuid = get_env_var('PROJECT_UUID');
|
||||||
|
$projectId = get_env_var('PROJECT_ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback if environment variables are completely missing in deployment
|
||||||
|
// Using values from db/config.php logic or current environment
|
||||||
|
if (!$projectUuid) $projectUuid = '36fb441e-8408-4101-afdc-7911dc065e36';
|
||||||
|
if (!$projectId) $projectId = '38960';
|
||||||
|
|
||||||
$baseUrl = 'https://flatlogic.com';
|
$baseUrl = 'https://flatlogic.com';
|
||||||
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
$responsesPath = $projectId ? "/projects/{$projectId}/ai-request" : null;
|
||||||
|
|||||||
80
api/ai_report.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../ai/LocalAIApi.php';
|
||||||
|
|
||||||
|
// Get JSON input
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
$target = $input['target'] ?? 'treatment_plan'; // symptoms, diagnosis, treatment_plan, translate
|
||||||
|
$symptoms = $input['symptoms'] ?? '';
|
||||||
|
$diagnosis = $input['diagnosis'] ?? '';
|
||||||
|
$currentValue = $input['current_value'] ?? ''; // For expanding symptoms or translation text
|
||||||
|
|
||||||
|
$systemPrompt = 'You are a professional medical assistant.';
|
||||||
|
$userPrompt = "";
|
||||||
|
|
||||||
|
switch ($target) {
|
||||||
|
case 'translate':
|
||||||
|
$text = $input['text'] ?? '';
|
||||||
|
$from = $input['from'] ?? 'English';
|
||||||
|
$to = $input['to'] ?? 'Arabic';
|
||||||
|
|
||||||
|
if (empty($text)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No text provided for translation.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$systemPrompt = "You are a professional translator specializing in medical terminology.";
|
||||||
|
$userPrompt = "Translate the following text from $from to $to. Return only the translated text without any explanations or extra characters.\n\nText: $text";
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'symptoms':
|
||||||
|
if (empty($currentValue)) {
|
||||||
|
$userPrompt = "Generate a list of common clinical symptoms for a general checkup in a professional medical format (HTML lists).";
|
||||||
|
} else {
|
||||||
|
$userPrompt = "Rewrite and expand the following patient symptoms into a professional clinical description using HTML (bullet points or paragraph). Maintain the original meaning but make it clearer and more detailed:\n\n" . strip_tags($currentValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'diagnosis':
|
||||||
|
if (empty($symptoms)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Please enter symptoms first to get a diagnosis suggestion.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$userPrompt = "Based on the following symptoms, suggest a list of potential differential diagnoses. Provide the response in a clear HTML list format.\n\nSymptoms:\n" . strip_tags($symptoms);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'treatment_plan':
|
||||||
|
default:
|
||||||
|
if (empty($symptoms) && empty($diagnosis)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No symptoms or diagnosis provided.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
$userPrompt = "Based on the following symptoms and diagnosis, please generate a concise treatment plan and medical report for the patient.\n\n";
|
||||||
|
if (!empty($symptoms)) {
|
||||||
|
$userPrompt .= "Symptoms:\n" . strip_tags($symptoms) . "\n\n";
|
||||||
|
}
|
||||||
|
if (!empty($diagnosis)) {
|
||||||
|
$userPrompt .= "Diagnosis:\n" . strip_tags($diagnosis) . "\n\n";
|
||||||
|
}
|
||||||
|
$userPrompt .= "Please provide the report in a clear, professional format using HTML tags (like <ul>, <li>, <strong>, etc.) for better readability.";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = LocalAIApi::createResponse([
|
||||||
|
'input' => [
|
||||||
|
['role' => 'system', 'content' => $systemPrompt],
|
||||||
|
['role' => 'user', 'content' => $userPrompt],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($response['success'])) {
|
||||||
|
$text = LocalAIApi::extractText($response);
|
||||||
|
echo json_encode(['success' => true, 'report' => trim($text)]);
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => $response['error'] ?? 'AI generation failed.']);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
359
api/appointments.php
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
// Prevent caching
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
header('Pragma: no-cache');
|
||||||
|
header('Expires: 0');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$lang = $_SESSION['lang'] ?? 'en';
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$id = $_GET['id'] ?? null;
|
||||||
|
if ($id) {
|
||||||
|
// Fetch single appointment
|
||||||
|
$stmt = $db->prepare("SELECT * FROM appointments WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
$appointment = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
echo json_encode($appointment);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$startStr = $_GET['start'] ?? null;
|
||||||
|
$endStr = $_GET['end'] ?? null;
|
||||||
|
$doctor_id = $_GET['doctor_id'] ?? null;
|
||||||
|
|
||||||
|
$events = [];
|
||||||
|
$businessHours = [];
|
||||||
|
|
||||||
|
// Fetch Appointments
|
||||||
|
$query = "
|
||||||
|
SELECT
|
||||||
|
a.id, a.start_time as start, a.end_time as end, a.reason, a.status,
|
||||||
|
a.patient_id, a.doctor_id, a.nurse_id, a.visit_type, a.address,
|
||||||
|
p.name as patient_name,
|
||||||
|
doc.name_$lang as doctor_name,
|
||||||
|
nur.name_$lang as nurse_name
|
||||||
|
FROM appointments a
|
||||||
|
JOIN patients p ON a.patient_id = p.id
|
||||||
|
LEFT JOIN employees doc ON a.doctor_id = doc.id
|
||||||
|
LEFT JOIN employees nur ON a.nurse_id = nur.id
|
||||||
|
WHERE 1=1";
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
if ($startStr) { $query .= " AND a.start_time >= ?"; $params[] = $startStr; }
|
||||||
|
if ($endStr) { $query .= " AND a.start_time <= ?"; $params[] = $endStr; }
|
||||||
|
if ($doctor_id) { $query .= " AND a.doctor_id = ?"; $params[] = $doctor_id; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare($query);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$appointments = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Appointments Fetch Error: " . $e->getMessage());
|
||||||
|
$appointments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($appointments as $a) {
|
||||||
|
$color = '#0d6efd'; // blue
|
||||||
|
if ($a['status'] === 'Completed') $color = '#198754'; // green
|
||||||
|
if ($a['status'] === 'Cancelled') $color = '#dc3545'; // red
|
||||||
|
if ($a['visit_type'] === 'Home') $color = '#fd7e14'; // orange for home visits
|
||||||
|
|
||||||
|
$providerName = $a['doctor_name'] ? $a['doctor_name'] : ($a['nurse_name'] . ' (' . __('nurse') . ')');
|
||||||
|
$title = $a['patient_name'] . ' - ' . $providerName;
|
||||||
|
if ($a['visit_type'] === 'Home') {
|
||||||
|
$title = '[🏠] ' . $title;
|
||||||
|
}
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => $a['id'],
|
||||||
|
'title' => $title,
|
||||||
|
'start' => $a['start'],
|
||||||
|
'end' => $a['end'],
|
||||||
|
'color' => $color,
|
||||||
|
'extendedProps' => [
|
||||||
|
'type' => 'appointment',
|
||||||
|
'patient_id' => $a['patient_id'],
|
||||||
|
'doctor_id' => $a['doctor_id'],
|
||||||
|
'nurse_id' => $a['nurse_id'],
|
||||||
|
'visit_type' => $a['visit_type'],
|
||||||
|
'address' => $a['address'],
|
||||||
|
'patient_name' => $a['patient_name'],
|
||||||
|
'doctor_name' => $a['doctor_name'],
|
||||||
|
'nurse_name' => $a['nurse_name'],
|
||||||
|
'status' => $a['status'],
|
||||||
|
'reason' => $a['reason']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch Public Holidays
|
||||||
|
$holidayQuery = "SELECT holiday_date as start, name_$lang as title FROM holidays WHERE 1=1";
|
||||||
|
$holidayParams = [];
|
||||||
|
if ($startStr) { $holidayQuery .= " AND holiday_date >= ?"; $holidayParams[] = date('Y-m-d', strtotime($startStr)); }
|
||||||
|
if ($endStr) { $holidayQuery .= " AND holiday_date <= ?"; $holidayParams[] = date('Y-m-d', strtotime($endStr)); }
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare($holidayQuery);
|
||||||
|
$stmt->execute($holidayParams);
|
||||||
|
$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$holidays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$s = get_system_settings();
|
||||||
|
$global_start = $s['working_hours_start'] ?? '08:00';
|
||||||
|
$global_end = $s['working_hours_end'] ?? '17:00';
|
||||||
|
|
||||||
|
foreach ($holidays as $h) {
|
||||||
|
// Render a visible block event in the time grid
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'hol_block_' . $h['start'],
|
||||||
|
'title' => __('holiday') . ': ' . $h['title'],
|
||||||
|
'start' => $h['start'] . 'T' . $global_start . ':00',
|
||||||
|
'end' => $h['start'] . 'T' . $global_end . ':00',
|
||||||
|
'allDay' => false,
|
||||||
|
'color' => '#dc3545',
|
||||||
|
'textColor' => '#fff',
|
||||||
|
'className' => 'public-holiday-event',
|
||||||
|
'extendedProps' => ['type' => 'public_holiday_block', 'blocks_selection' => true]
|
||||||
|
];
|
||||||
|
|
||||||
|
// Render daily blocks for time grid
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'hol_bg_' . $h['start'],
|
||||||
|
'start' => $h['start'] . 'T00:00:00',
|
||||||
|
'end' => $h['start'] . 'T23:59:59',
|
||||||
|
'allDay' => false,
|
||||||
|
'display' => 'background',
|
||||||
|
'backgroundColor' => 'rgba(255, 193, 7, 0.5)',
|
||||||
|
'overlap' => false,
|
||||||
|
'extendedProps' => ['type' => 'public_holiday', 'blocks_selection' => true]
|
||||||
|
];
|
||||||
|
// Visible event strip across the top
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'hol_title_' . $h['start'],
|
||||||
|
'title' => __('holiday') . ': ' . $h['title'],
|
||||||
|
'start' => $h['start'],
|
||||||
|
'end' => $h['start'],
|
||||||
|
'allDay' => true,
|
||||||
|
'color' => '#ffc107',
|
||||||
|
'textColor' => '#000',
|
||||||
|
'extendedProps' => ['type' => 'public_holiday']
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch Doctor Holidays (from Leave Requests)
|
||||||
|
// Updated to join employees instead of doctors
|
||||||
|
$docHolidayQuery = "
|
||||||
|
SELECT lr.id, lr.start_date, lr.end_date, lr.reason as note,
|
||||||
|
lr.employee_id as doctor_id,
|
||||||
|
e.name_$lang as doctor_name
|
||||||
|
FROM leave_requests lr
|
||||||
|
JOIN employees e ON lr.employee_id = e.id
|
||||||
|
WHERE lr.status = 'Approved'";
|
||||||
|
|
||||||
|
$docHolidayParams = [];
|
||||||
|
|
||||||
|
// Date filtering for doctor holidays (ranges)
|
||||||
|
if ($startStr && $endStr) {
|
||||||
|
$docHolidayQuery .= " AND lr.start_date <= ? AND lr.end_date >= ?";
|
||||||
|
$docHolidayParams[] = date('Y-m-d', strtotime($endStr));
|
||||||
|
$docHolidayParams[] = date('Y-m-d', strtotime($startStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($doctor_id) {
|
||||||
|
$docHolidayQuery .= " AND lr.employee_id = ?";
|
||||||
|
$docHolidayParams[] = $doctor_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare($docHolidayQuery);
|
||||||
|
$stmt->execute($docHolidayParams);
|
||||||
|
$docHolidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$docHolidays = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($docHolidays as $dh) {
|
||||||
|
$title = $dh['doctor_name'] . ' - ' . ($dh['note'] ?: 'Holiday');
|
||||||
|
$endDayStr = date('Y-m-d', strtotime($dh['end_date'] . ' +1 day'));
|
||||||
|
|
||||||
|
$currentDate = strtotime($dh['start_date']);
|
||||||
|
$endDate = strtotime($dh['end_date']);
|
||||||
|
|
||||||
|
$isFilteredDoctor = ($doctor_id && $doctor_id == $dh['doctor_id']);
|
||||||
|
|
||||||
|
// Output background events for each day individually
|
||||||
|
while ($currentDate <= $endDate) {
|
||||||
|
$dateStr = date('Y-m-d', $currentDate);
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'doc_hol_block_' . $dh['id'] . '_' . $dateStr,
|
||||||
|
'title' => 'Holiday: ' . $dh['doctor_name'],
|
||||||
|
'start' => $dateStr . 'T' . $global_start . ':00',
|
||||||
|
'end' => $dateStr . 'T' . $global_end . ':00',
|
||||||
|
'allDay' => false,
|
||||||
|
'color' => '#ffc107',
|
||||||
|
'textColor' => '#000',
|
||||||
|
'className' => 'doctor-holiday-event',
|
||||||
|
'extendedProps' => ['type' => 'doctor_holiday_block', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor]
|
||||||
|
];
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'doc_hol_bg_' . $dh['id'] . '_' . $dateStr,
|
||||||
|
'start' => $dateStr . 'T00:00:00',
|
||||||
|
'end' => $dateStr . 'T23:59:59',
|
||||||
|
'display' => 'background',
|
||||||
|
'allDay' => false,
|
||||||
|
'backgroundColor' => $isFilteredDoctor ? 'rgba(255, 193, 7, 0.5)' : 'rgba(255, 193, 7, 0.15)',
|
||||||
|
'overlap' => !$isFilteredDoctor,
|
||||||
|
'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor]
|
||||||
|
];
|
||||||
|
|
||||||
|
$currentDate = strtotime('+1 day', $currentDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
$events[] = [
|
||||||
|
'id' => 'doc_hol_' . $dh['id'],
|
||||||
|
'title' => $title,
|
||||||
|
'start' => $dh['start_date'],
|
||||||
|
'end' => $endDayStr,
|
||||||
|
'allDay' => true,
|
||||||
|
'color' => '#ffc107',
|
||||||
|
'textColor' => '#000',
|
||||||
|
'extendedProps' => ['type' => 'doctor_holiday', 'doctor_id' => $dh['doctor_id'], 'blocks_selection' => $isFilteredDoctor]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Business Hours (Global Default since individual schedules are removed)
|
||||||
|
$st = $s['working_hours_start'] ?? '08:00';
|
||||||
|
$et = $s['working_hours_end'] ?? '17:00';
|
||||||
|
$businessHours = [
|
||||||
|
[
|
||||||
|
'daysOfWeek' => [0, 1, 2, 3, 4, 5, 6],
|
||||||
|
'startTime' => $st,
|
||||||
|
'endTime' => $et
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'events' => $events,
|
||||||
|
'businessHours' => $businessHours,
|
||||||
|
'settings' => [
|
||||||
|
'working_hours_start' => $global_start,
|
||||||
|
'working_hours_end' => $global_end
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkDoctorHoliday($db, $doctor_id, $start_time) {
|
||||||
|
if (!$doctor_id || !$start_time) return false;
|
||||||
|
$date = date('Y-m-d', strtotime($start_time));
|
||||||
|
try {
|
||||||
|
// Query leave_requests directly using employee_id (which is $doctor_id)
|
||||||
|
$stmt = $db->prepare("SELECT COUNT(*) FROM leave_requests WHERE employee_id = ? AND status = 'Approved' AND ? BETWEEN start_date AND end_date");
|
||||||
|
$stmt->execute([$doctor_id, $date]);
|
||||||
|
return $stmt->fetchColumn() > 0;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
error_log("Check Holiday Error: " . $e->getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (!$input) {
|
||||||
|
$input = $_POST;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($input)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No input data received']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $input['action'] ?? '';
|
||||||
|
|
||||||
|
if (empty($action)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'No action specified']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'create') {
|
||||||
|
$patient_id = $input['patient_id'] ?? '';
|
||||||
|
$doctor_id = $input['doctor_id'] ?: null; // Nullable
|
||||||
|
$nurse_id = $input['nurse_id'] ?: null; // Nullable
|
||||||
|
$visit_type = $input['visit_type'] ?? 'Clinic';
|
||||||
|
$address = $input['address'] ?? '';
|
||||||
|
$start_time = $input['start_time'] ?? '';
|
||||||
|
$reason = $input['reason'] ?? '';
|
||||||
|
|
||||||
|
if ($patient_id && ($doctor_id || $nurse_id) && $start_time) {
|
||||||
|
// Check for holiday conflict if doctor assigned
|
||||||
|
if ($doctor_id && checkDoctorHoliday($db, $doctor_id, $start_time)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Doctor is on holiday on this date.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("INSERT INTO appointments (patient_id, doctor_id, nurse_id, visit_type, address, start_time, end_time, reason) VALUES (?, ?, ?, ?, ?, ?, DATE_ADD(?, INTERVAL 30 MINUTE), ?)");
|
||||||
|
$stmt->execute([$patient_id, $doctor_id, $nurse_id, $visit_type, $address, $start_time, $start_time, $reason]);
|
||||||
|
echo json_encode(['success' => true, 'id' => $db->lastInsertId()]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing fields']);
|
||||||
|
}
|
||||||
|
} elseif ($action === 'update') {
|
||||||
|
$id = $input['id'] ?? '';
|
||||||
|
$patient_id = $input['patient_id'] ?? '';
|
||||||
|
$doctor_id = $input['doctor_id'] ?: null;
|
||||||
|
$nurse_id = $input['nurse_id'] ?: null;
|
||||||
|
$visit_type = $input['visit_type'] ?? 'Clinic';
|
||||||
|
$address = $input['address'] ?? '';
|
||||||
|
$start_time = $input['start_time'] ?? '';
|
||||||
|
$status = $input['status'] ?? 'Scheduled';
|
||||||
|
$reason = $input['reason'] ?? '';
|
||||||
|
|
||||||
|
if ($id && $patient_id && ($doctor_id || $nurse_id) && $start_time) {
|
||||||
|
// Check for holiday conflict
|
||||||
|
if ($doctor_id && checkDoctorHoliday($db, $doctor_id, $start_time)) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Doctor is on holiday on this date.']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("UPDATE appointments SET patient_id = ?, doctor_id = ?, nurse_id = ?, visit_type = ?, address = ?, start_time = ?, end_time = DATE_ADD(?, INTERVAL 30 MINUTE), status = ?, reason = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$patient_id, $doctor_id, $nurse_id, $visit_type, $address, $start_time, $start_time, $status, $reason, $id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing fields']);
|
||||||
|
}
|
||||||
|
} elseif ($action === 'delete') {
|
||||||
|
$id = $input['id'] ?? '';
|
||||||
|
if ($id) {
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("DELETE FROM appointments WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing ID']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid action']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
274
api/billing.php
Normal file
@ -0,0 +1,274 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
$action = $_POST['action'] ?? $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'get_bill_details') {
|
||||||
|
$visit_id = $_GET['visit_id'] ?? 0;
|
||||||
|
$bill_id_param = $_GET['bill_id'] ?? 0;
|
||||||
|
|
||||||
|
if (!$visit_id && !$bill_id_param) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Visit ID or Bill ID required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($bill_id_param) {
|
||||||
|
$stmt = $db->prepare("SELECT visit_id FROM bills WHERE id = ?");
|
||||||
|
$stmt->execute([$bill_id_param]);
|
||||||
|
$visit_id = $stmt->fetchColumn();
|
||||||
|
|
||||||
|
if (!$visit_id) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Bill not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Get Visit & Patient Info
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT v.*, p.name as patient_name, p.insurance_company_id, ic.name_en as insurance_name
|
||||||
|
FROM visits v
|
||||||
|
JOIN patients p ON v.patient_id = p.id
|
||||||
|
LEFT JOIN insurance_companies ic ON p.insurance_company_id = ic.id
|
||||||
|
WHERE v.id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$visit_id]);
|
||||||
|
$visit = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$visit) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Visit not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Get or Create Bill
|
||||||
|
$stmt = $db->prepare("SELECT * FROM bills WHERE visit_id = ?");
|
||||||
|
$stmt->execute([$visit_id]);
|
||||||
|
$bill = $stmt->fetch();
|
||||||
|
|
||||||
|
if (!$bill) {
|
||||||
|
$stmt = $db->prepare("INSERT INTO bills (patient_id, visit_id, status, created_at) VALUES (?, ?, 'Pending', NOW())");
|
||||||
|
$stmt->execute([$visit['patient_id'], $visit_id]);
|
||||||
|
$bill_id = $db->lastInsertId();
|
||||||
|
|
||||||
|
// Re-fetch
|
||||||
|
$stmt = $db->prepare("SELECT * FROM bills WHERE id = ?");
|
||||||
|
$stmt->execute([$bill_id]);
|
||||||
|
$bill = $stmt->fetch();
|
||||||
|
} else {
|
||||||
|
$bill_id = $bill['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- AUTO-ADD ITEMS FROM OTHER DEPARTMENTS ---
|
||||||
|
// Fetch existing items to prevent duplicates
|
||||||
|
$stmt = $db->prepare("SELECT description FROM bill_items WHERE bill_id = ?");
|
||||||
|
$stmt->execute([$bill_id]);
|
||||||
|
$existing_items = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
|
// Helper to check and add
|
||||||
|
$added_count = 0;
|
||||||
|
|
||||||
|
// 1. X-Rays
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT xt.name_en, xt.price
|
||||||
|
FROM xray_inquiry_items xii
|
||||||
|
JOIN xray_inquiries xi ON xii.inquiry_id = xi.id
|
||||||
|
JOIN xray_tests xt ON xii.xray_id = xt.id
|
||||||
|
WHERE xi.visit_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$visit_id]);
|
||||||
|
$xrays = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($xrays as $x) {
|
||||||
|
$desc = "X-Ray: " . $x['name_en'];
|
||||||
|
if (!in_array($desc, $existing_items)) {
|
||||||
|
$stmt = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$bill_id, $desc, $x['price']]);
|
||||||
|
$existing_items[] = $desc; // Update local cache
|
||||||
|
$added_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Labs
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT lt.name_en, lt.price
|
||||||
|
FROM inquiry_tests it
|
||||||
|
JOIN laboratory_inquiries li ON it.inquiry_id = li.id
|
||||||
|
JOIN laboratory_tests lt ON it.test_id = lt.id
|
||||||
|
WHERE li.visit_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$visit_id]);
|
||||||
|
$labs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($labs as $l) {
|
||||||
|
$desc = "Lab: " . $l['name_en'];
|
||||||
|
if (!in_array($desc, $existing_items)) {
|
||||||
|
$stmt = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$bill_id, $desc, $l['price']]);
|
||||||
|
$existing_items[] = $desc;
|
||||||
|
$added_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Drugs (Prescriptions)
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT vp.drug_name, d.price
|
||||||
|
FROM visit_prescriptions vp
|
||||||
|
LEFT JOIN drugs d ON (vp.drug_name = d.name_en OR vp.drug_name = d.name_ar)
|
||||||
|
WHERE vp.visit_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$visit_id]);
|
||||||
|
$drugs = $stmt->fetchAll();
|
||||||
|
|
||||||
|
foreach ($drugs as $d) {
|
||||||
|
$price = $d['price'] ?: 0; // Default to 0 if not found
|
||||||
|
$desc = "Pharmacy: " . $d['drug_name'];
|
||||||
|
if (!in_array($desc, $existing_items)) {
|
||||||
|
$stmt = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$bill_id, $desc, $price]);
|
||||||
|
$existing_items[] = $desc;
|
||||||
|
$added_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If items were added, update the bill total
|
||||||
|
if ($added_count > 0) {
|
||||||
|
updateBillTotal($db, $bill_id);
|
||||||
|
// Re-fetch bill to get updated total if needed (though calculate total below handles it)
|
||||||
|
$stmt = $db->prepare("SELECT * FROM bills WHERE id = ?");
|
||||||
|
$stmt->execute([$bill_id]);
|
||||||
|
$bill = $stmt->fetch();
|
||||||
|
}
|
||||||
|
// ---------------------------------------------
|
||||||
|
|
||||||
|
// 3. Get Bill Items (Fresh)
|
||||||
|
$stmt = $db->prepare("SELECT * FROM bill_items WHERE bill_id = ?");
|
||||||
|
$stmt->execute([$bill['id']]);
|
||||||
|
$items = $stmt->fetchAll();
|
||||||
|
|
||||||
|
// 4. Calculate Totals (if not synced)
|
||||||
|
$total = 0;
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$total += $item['amount'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return Data
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'visit' => $visit,
|
||||||
|
'bill' => $bill,
|
||||||
|
'items' => $items,
|
||||||
|
'calculated_total' => $total,
|
||||||
|
'has_insurance' => !empty($visit['insurance_company_id']),
|
||||||
|
'insurance_name' => $visit['insurance_name']
|
||||||
|
]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elseif ($action === 'add_item') {
|
||||||
|
$bill_id = $_POST['bill_id'] ?? 0;
|
||||||
|
$description = $_POST['description'] ?? '';
|
||||||
|
$amount = $_POST['amount'] ?? 0;
|
||||||
|
|
||||||
|
if (!$bill_id || !$description || !$amount) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing fields']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("INSERT INTO bill_items (bill_id, description, amount) VALUES (?, ?, ?)");
|
||||||
|
$stmt->execute([$bill_id, $description, $amount]);
|
||||||
|
|
||||||
|
// Update Bill Total
|
||||||
|
updateBillTotal($db, $bill_id);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elseif ($action === 'remove_item') {
|
||||||
|
$item_id = $_POST['item_id'] ?? 0;
|
||||||
|
|
||||||
|
if (!$item_id) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Item ID required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get bill_id first
|
||||||
|
$stmt = $db->prepare("SELECT bill_id FROM bill_items WHERE id = ?");
|
||||||
|
$stmt->execute([$item_id]);
|
||||||
|
$row = $stmt->fetch();
|
||||||
|
|
||||||
|
if ($row) {
|
||||||
|
$stmt = $db->prepare("DELETE FROM bill_items WHERE id = ?");
|
||||||
|
$stmt->execute([$item_id]);
|
||||||
|
updateBillTotal($db, $row['bill_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elseif ($action === 'update_totals') {
|
||||||
|
$bill_id = $_POST['bill_id'] ?? 0;
|
||||||
|
$insurance_covered = $_POST['insurance_covered'] ?? 0;
|
||||||
|
$patient_payable = $_POST['patient_payable'] ?? 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("UPDATE bills SET insurance_covered = ?, patient_payable = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$insurance_covered, $patient_payable, $bill_id]);
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elseif ($action === 'complete_payment') {
|
||||||
|
$bill_id = $_POST['bill_id'] ?? 0;
|
||||||
|
$payment_method = $_POST['payment_method'] ?? 'Cash';
|
||||||
|
$notes = $_POST['notes'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db->beginTransaction();
|
||||||
|
|
||||||
|
// Update Bill
|
||||||
|
$stmt = $db->prepare("UPDATE bills SET status = 'Paid', payment_method = ?, notes = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$payment_method, $notes, $bill_id]);
|
||||||
|
|
||||||
|
// Get Visit ID
|
||||||
|
$stmt = $db->prepare("SELECT visit_id FROM bills WHERE id = ?");
|
||||||
|
$stmt->execute([$bill_id]);
|
||||||
|
$bill = $stmt->fetch();
|
||||||
|
|
||||||
|
// Update Visit
|
||||||
|
if ($bill && $bill['visit_id']) {
|
||||||
|
$stmt = $db->prepare("UPDATE visits SET status = 'Completed', checkout_time = NOW() WHERE id = ?");
|
||||||
|
$stmt->execute([$bill['visit_id']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->commit();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$db->rollBack();
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBillTotal($db, $bill_id) {
|
||||||
|
$stmt = $db->prepare("SELECT SUM(amount) FROM bill_items WHERE bill_id = ?");
|
||||||
|
$stmt->execute([$bill_id]);
|
||||||
|
$total = $stmt->fetchColumn() ?: 0;
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE bills SET total_amount = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$total, $bill_id]);
|
||||||
|
}
|
||||||
90
api/biometric_push.php
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Database connection failed']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read input
|
||||||
|
$input = file_get_contents('php://input');
|
||||||
|
$data = json_decode($input, true);
|
||||||
|
|
||||||
|
if (!$data) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid JSON']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic Auth (API Key)
|
||||||
|
// In production, check against biometric_devices table
|
||||||
|
$api_key = $data['api_key'] ?? '';
|
||||||
|
if ($api_key !== 'test_key') {
|
||||||
|
// Check DB
|
||||||
|
$stmt = $pdo->prepare("SELECT id FROM biometric_devices WHERE api_key = ? AND status = 1");
|
||||||
|
$stmt->execute([$api_key]);
|
||||||
|
if (!$stmt->fetch()) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid API Key']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Data
|
||||||
|
$employee_id = $data['employee_id'] ?? null;
|
||||||
|
$timestamp = $data['timestamp'] ?? date('Y-m-d H:i:s'); // ISO 8601 or Y-m-d H:i:s
|
||||||
|
$type = $data['type'] ?? 'check_in'; // check_in or check_out
|
||||||
|
|
||||||
|
if (!$employee_id) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing employee_id']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine status based on time (simple logic)
|
||||||
|
$time = date('H:i:s', strtotime($timestamp));
|
||||||
|
$date = date('Y-m-d', strtotime($timestamp));
|
||||||
|
$status = 'Present';
|
||||||
|
if ($type === 'check_in' && $time > '09:00:00') {
|
||||||
|
$status = 'Late';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO attendance_logs (employee_id, date, check_in, check_out, status, source) VALUES (?, ?, ?, ?, ?, 'Biometric Device')");
|
||||||
|
|
||||||
|
$check_in = ($type === 'check_in') ? date('Y-m-d H:i:s', strtotime($timestamp)) : null;
|
||||||
|
$check_out = ($type === 'check_out') ? date('Y-m-d H:i:s', strtotime($timestamp)) : null;
|
||||||
|
|
||||||
|
// Check if entry exists for this date to update instead of insert?
|
||||||
|
// For simplicity, we just insert logs. A real system might merge them.
|
||||||
|
// Let's try to find an existing log for today
|
||||||
|
$existing = $pdo->prepare("SELECT id FROM attendance_logs WHERE employee_id = ? AND date = ? ORDER BY id DESC LIMIT 1");
|
||||||
|
$existing->execute([$employee_id, $date]);
|
||||||
|
$log = $existing->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($log) {
|
||||||
|
if ($type === 'check_in') {
|
||||||
|
// Maybe they checked in again? Update check_in if null
|
||||||
|
$upd = $pdo->prepare("UPDATE attendance_logs SET check_in = ? WHERE id = ? AND check_in IS NULL");
|
||||||
|
$upd->execute([$check_in, $log['id']]);
|
||||||
|
} else {
|
||||||
|
// Check out
|
||||||
|
$upd = $pdo->prepare("UPDATE attendance_logs SET check_out = ? WHERE id = ?");
|
||||||
|
$upd->execute([$check_out, $log['id']]);
|
||||||
|
}
|
||||||
|
$msg = "Updated existing log";
|
||||||
|
} else {
|
||||||
|
$stmt->execute([$employee_id, $date, $check_in, $check_out, $status]);
|
||||||
|
$msg = "Created new log";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => $msg]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
48
api/cities.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$method = $_SERVER['REQUEST_METHOD'];
|
||||||
|
|
||||||
|
if ($method === 'POST') {
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true) ?? $_POST;
|
||||||
|
$action = $input['action'] ?? '';
|
||||||
|
|
||||||
|
if ($action === 'add' || $action === 'add_city') {
|
||||||
|
$name_en = $input['name_en'] ?? $input['name'] ?? '';
|
||||||
|
$name_ar = $input['name_ar'] ?? $input['name'] ?? '';
|
||||||
|
|
||||||
|
if ($name_en && $name_ar) {
|
||||||
|
try {
|
||||||
|
$stmt = $db->prepare("INSERT INTO cities (name_en, name_ar) VALUES (?, ?)");
|
||||||
|
$stmt->execute([$name_en, $name_ar]);
|
||||||
|
$id = $db->lastInsertId();
|
||||||
|
|
||||||
|
$lang = $_SESSION['lang'] ?? 'en';
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'id' => $id,
|
||||||
|
'name_en' => $name_en,
|
||||||
|
'name_ar' => $name_ar,
|
||||||
|
'city' => [
|
||||||
|
'id' => $id,
|
||||||
|
'name' => ($lang === 'ar' && !empty($name_ar)) ? $name_ar : $name_en
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing fields']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid action']);
|
||||||
|
}
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid request method']);
|
||||||
32
api/doctor_holidays.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
|
||||||
|
// Prevent caching
|
||||||
|
header('Cache-Control: no-cache, no-store, must-revalidate');
|
||||||
|
header('Pragma: no-cache');
|
||||||
|
header('Expires: 0');
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$doctor_id = $_GET['doctor_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$doctor_id) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing doctor_id']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// $doctor_id is expected to be employee_id now
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT start_date, end_date, reason
|
||||||
|
FROM leave_requests
|
||||||
|
WHERE employee_id = ? AND status = 'Approved' AND end_date >= CURDATE()
|
||||||
|
");
|
||||||
|
$stmt->execute([$doctor_id]);
|
||||||
|
$holidays = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'holidays' => $holidays]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'DB Error: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
40
api/inventory.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../helpers.php';
|
||||||
|
require_once __DIR__ . '/../includes/auth.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
// Check if user is logged in
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['error' => 'Unauthorized']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check permission
|
||||||
|
if (!has_permission('inventory')) {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['error' => 'Forbidden: Stock Management permission required']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
if ($action === 'get_batches') {
|
||||||
|
$item_id = $_GET['item_id'] ?? 0;
|
||||||
|
if (!$item_id) {
|
||||||
|
echo json_encode([]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT id, batch_number, quantity, expiry_date, cost_price FROM inventory_batches WHERE item_id = ? AND quantity > 0 ORDER BY expiry_date ASC");
|
||||||
|
$stmt->execute([$item_id]);
|
||||||
|
$batches = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode($batches);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['error' => 'Invalid action']);
|
||||||
51
api/patients.php
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch ($action) {
|
||||||
|
case 'search':
|
||||||
|
$q = $_GET['q'] ?? '';
|
||||||
|
|
||||||
|
if (empty($q)) {
|
||||||
|
echo json_encode(['results' => []]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search by name, phone or id (patient number)
|
||||||
|
$sql = "SELECT id, name, phone, civil_id FROM patients WHERE name LIKE ? OR phone LIKE ? OR civil_id LIKE ? OR id = ? LIMIT 20";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$term = "%$q%";
|
||||||
|
$id_term = intval($q); // for exact match on patient number
|
||||||
|
$stmt->execute([$term, $term, $term, $id_term]);
|
||||||
|
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Format results for select2
|
||||||
|
$formatted_results = array_map(function($p) {
|
||||||
|
$patient_number = sprintf('%06d', $p['id']);
|
||||||
|
$display_text = $patient_number . ' - ' . $p['name'];
|
||||||
|
if (!empty($p['phone'])) {
|
||||||
|
$display_text .= ' - ' . $p['phone'];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
'id' => $p['id'],
|
||||||
|
'text' => $display_text,
|
||||||
|
'name' => $p['name'],
|
||||||
|
'phone' => $p['phone']
|
||||||
|
];
|
||||||
|
}, $results);
|
||||||
|
|
||||||
|
echo json_encode(['results' => $formatted_results]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => 'Invalid action']);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
483
api/pharmacy.php
Normal file
@ -0,0 +1,483 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 10;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
switch ($action) {
|
||||||
|
case 'get_stock':
|
||||||
|
// List all drugs with total stock quantity
|
||||||
|
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
|
||||||
|
COALESCE(SUM(b.quantity), 0) as total_stock
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
GROUP BY d.id
|
||||||
|
ORDER BY d.name_en ASC";
|
||||||
|
$stmt = $pdo->query($sql);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_low_stock':
|
||||||
|
// Count total for pagination
|
||||||
|
$countSql = "SELECT COUNT(*) FROM (
|
||||||
|
SELECT d.id
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
GROUP BY d.id
|
||||||
|
HAVING COALESCE(SUM(b.quantity), 0) <= MAX(d.reorder_level)
|
||||||
|
) as total";
|
||||||
|
$total = $pdo->query($countSql)->fetchColumn();
|
||||||
|
|
||||||
|
// List drugs where total stock is <= reorder_level
|
||||||
|
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
|
||||||
|
COALESCE(SUM(b.quantity), 0) as total_stock
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
GROUP BY d.id
|
||||||
|
HAVING total_stock <= MAX(d.reorder_level)
|
||||||
|
ORDER BY total_stock ASC
|
||||||
|
LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_expired':
|
||||||
|
// Count total
|
||||||
|
$countSql = "SELECT COUNT(*)
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
JOIN drugs d ON b.drug_id = d.id
|
||||||
|
LEFT JOIN suppliers s ON b.supplier_id = s.id
|
||||||
|
WHERE b.expiry_date < CURDATE() AND b.quantity > 0";
|
||||||
|
$total = $pdo->query($countSql)->fetchColumn();
|
||||||
|
|
||||||
|
// List batches that have expired and still have quantity
|
||||||
|
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
|
||||||
|
d.name_en as drug_name, d.name_ar as drug_name_ar,
|
||||||
|
s.name_en as supplier_name
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
JOIN drugs d ON b.drug_id = d.id
|
||||||
|
LEFT JOIN suppliers s ON b.supplier_id = s.id
|
||||||
|
WHERE b.expiry_date < CURDATE() AND b.quantity > 0
|
||||||
|
ORDER BY b.expiry_date ASC
|
||||||
|
LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_near_expiry':
|
||||||
|
$days = $_GET['days'] ?? 90;
|
||||||
|
|
||||||
|
// Count total
|
||||||
|
$countSql = "SELECT COUNT(*)
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
JOIN drugs d ON b.drug_id = d.id
|
||||||
|
LEFT JOIN suppliers s ON b.supplier_id = s.id
|
||||||
|
WHERE b.expiry_date >= CURDATE()
|
||||||
|
AND b.expiry_date <= DATE_ADD(CURDATE(), INTERVAL ? DAY)
|
||||||
|
AND b.quantity > 0";
|
||||||
|
$countStmt = $pdo->prepare($countSql);
|
||||||
|
$countStmt->execute([$days]);
|
||||||
|
$total = $countStmt->fetchColumn();
|
||||||
|
|
||||||
|
// List batches expiring in the next X days
|
||||||
|
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
|
||||||
|
d.name_en as drug_name, d.name_ar as drug_name_ar,
|
||||||
|
s.name_en as supplier_name,
|
||||||
|
DATEDIFF(b.expiry_date, CURDATE()) as days_remaining
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
JOIN drugs d ON b.drug_id = d.id
|
||||||
|
LEFT JOIN suppliers s ON b.supplier_id = s.id
|
||||||
|
WHERE b.expiry_date >= CURDATE()
|
||||||
|
AND b.expiry_date <= DATE_ADD(CURDATE(), INTERVAL :days DAY)
|
||||||
|
AND b.quantity > 0
|
||||||
|
ORDER BY b.expiry_date ASC
|
||||||
|
LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':days', $days, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_batches':
|
||||||
|
$drug_id = $_GET['drug_id'] ?? 0;
|
||||||
|
if (!$drug_id) throw new Exception("Drug ID required");
|
||||||
|
|
||||||
|
$sql = "SELECT b.*, s.name_en as supplier_name
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
LEFT JOIN suppliers s ON b.supplier_id = s.id
|
||||||
|
WHERE b.drug_id = ? AND b.quantity > 0
|
||||||
|
ORDER BY b.expiry_date ASC";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute([$drug_id]);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'add_stock':
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method");
|
||||||
|
|
||||||
|
$drug_id = $_POST['drug_id'] ?? 0;
|
||||||
|
$batch_number = $_POST['batch_number'] ?? '';
|
||||||
|
$expiry_date = $_POST['expiry_date'] ?? '';
|
||||||
|
$quantity = $_POST['quantity'] ?? 0;
|
||||||
|
$cost_price = $_POST['cost_price'] ?? 0;
|
||||||
|
$sale_price = $_POST['sale_price'] ?? 0;
|
||||||
|
$supplier_id = !empty($_POST['supplier_id']) ? $_POST['supplier_id'] : null;
|
||||||
|
|
||||||
|
if (!$drug_id || !$batch_number || !$expiry_date || !$quantity) {
|
||||||
|
throw new Exception("Missing required fields");
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pharmacy_batches
|
||||||
|
(drug_id, batch_number, expiry_date, quantity, cost_price, sale_price, supplier_id, received_date)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE())");
|
||||||
|
$stmt->execute([$drug_id, $batch_number, $expiry_date, $quantity, $cost_price, $sale_price, $supplier_id]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Stock added successfully']);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'search_drugs':
|
||||||
|
$q = $_GET['q'] ?? '';
|
||||||
|
$sql = "SELECT d.id, d.name_en, d.name_ar, d.sku, d.price as default_price,
|
||||||
|
(SELECT sale_price FROM pharmacy_batches pb WHERE pb.drug_id = d.id AND pb.quantity > 0 AND pb.expiry_date >= CURDATE() ORDER BY pb.expiry_date ASC LIMIT 1) as batch_price,
|
||||||
|
COALESCE(SUM(b.quantity), 0) as stock
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
WHERE (d.name_en LIKE ? OR d.name_ar LIKE ? OR d.sku LIKE ?)
|
||||||
|
GROUP BY d.id
|
||||||
|
LIMIT 20";
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$term = "%$q%";
|
||||||
|
$stmt->execute([$term, $term, $term]);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'create_sale':
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') throw new Exception("Invalid method");
|
||||||
|
$input = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($input['items'])) throw new Exception("No items in sale");
|
||||||
|
|
||||||
|
// Check Settings
|
||||||
|
$allow_negative = false;
|
||||||
|
try {
|
||||||
|
$settingStmt = $pdo->prepare("SELECT setting_value FROM settings WHERE setting_key = 'allow_negative_stock'");
|
||||||
|
$settingStmt->execute();
|
||||||
|
$allow_negative = (bool)$settingStmt->fetchColumn();
|
||||||
|
} catch (Exception $e) { /* ignore */ }
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create Sale Record
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pharmacy_sales (patient_id, visit_id, total_amount, payment_method, status) VALUES (?, ?, ?, ?, 'completed')");
|
||||||
|
$stmt->execute([
|
||||||
|
$input['patient_id'] ?? null,
|
||||||
|
$input['visit_id'] ?? null,
|
||||||
|
$input['total_amount'] ?? 0,
|
||||||
|
$input['payment_method'] ?? 'cash'
|
||||||
|
]);
|
||||||
|
$sale_id = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
// Process Items
|
||||||
|
foreach ($input['items'] as $item) {
|
||||||
|
$drug_id = $item['drug_id'];
|
||||||
|
$qty_needed = $item['quantity'];
|
||||||
|
$unit_price = $item['price']; // Or fetch from batch? Use provided price for now.
|
||||||
|
|
||||||
|
// Fetch available batches (FIFO)
|
||||||
|
$batch_stmt = $pdo->prepare("SELECT id, quantity FROM pharmacy_batches WHERE drug_id = ? AND quantity > 0 ORDER BY expiry_date ASC FOR UPDATE");
|
||||||
|
$batch_stmt->execute([$drug_id]);
|
||||||
|
$batches = $batch_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$qty_remaining = $qty_needed;
|
||||||
|
|
||||||
|
foreach ($batches as $batch) {
|
||||||
|
if ($qty_remaining <= 0) break;
|
||||||
|
|
||||||
|
$take = min($batch['quantity'], $qty_remaining);
|
||||||
|
|
||||||
|
// Deduct from batch
|
||||||
|
$update = $pdo->prepare("UPDATE pharmacy_batches SET quantity = quantity - ? WHERE id = ?");
|
||||||
|
$update->execute([$take, $batch['id']]);
|
||||||
|
|
||||||
|
// Add to sale items
|
||||||
|
$item_stmt = $pdo->prepare("INSERT INTO pharmacy_sale_items (sale_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
|
$item_stmt->execute([$sale_id, $drug_id, $batch['id'], $take, $unit_price, $take * $unit_price]);
|
||||||
|
|
||||||
|
$qty_remaining -= $take;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($qty_remaining > 0) {
|
||||||
|
if ($allow_negative) {
|
||||||
|
// Record sale for remaining quantity without batch
|
||||||
|
$item_stmt = $pdo->prepare("INSERT INTO pharmacy_sale_items (sale_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
|
$item_stmt->execute([$sale_id, $drug_id, null, $qty_remaining, $unit_price, $qty_remaining * $unit_price]);
|
||||||
|
} else {
|
||||||
|
throw new Exception("Insufficient stock for drug ID: $drug_id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'sale_id' => $sale_id]);
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_sales':
|
||||||
|
// List recent sales
|
||||||
|
$sql = "SELECT s.*, p.name as patient_name
|
||||||
|
FROM pharmacy_sales s
|
||||||
|
LEFT JOIN patients p ON s.patient_id = p.id
|
||||||
|
ORDER BY s.created_at DESC LIMIT 50";
|
||||||
|
$stmt = $pdo->query($sql);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_sale_details':
|
||||||
|
$sale_id = $_GET['sale_id'] ?? 0;
|
||||||
|
if (!$sale_id) throw new Exception("Sale ID required");
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("SELECT s.*, p.name as patient_name FROM pharmacy_sales s LEFT JOIN patients p ON s.patient_id = p.id WHERE s.id = ?");
|
||||||
|
$stmt->execute([$sale_id]);
|
||||||
|
$sale = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$sale) throw new Exception("Sale not found");
|
||||||
|
|
||||||
|
$items_stmt = $pdo->prepare("SELECT i.*, d.name_en as drug_name
|
||||||
|
FROM pharmacy_sale_items i
|
||||||
|
JOIN drugs d ON i.drug_id = d.id
|
||||||
|
WHERE i.sale_id = ?");
|
||||||
|
$items_stmt->execute([$sale_id]);
|
||||||
|
$sale['items'] = $items_stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode($sale);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'get_report':
|
||||||
|
$type = $_GET['type'] ?? 'inventory_valuation';
|
||||||
|
$startDate = $_GET['start_date'] ?? date('Y-m-01');
|
||||||
|
$endDate = $_GET['end_date'] ?? date('Y-m-d');
|
||||||
|
|
||||||
|
if ($type === 'inventory_valuation') {
|
||||||
|
// Count distinct drugs in stock for pagination
|
||||||
|
$countSql = "SELECT COUNT(DISTINCT d.id)
|
||||||
|
FROM drugs d
|
||||||
|
JOIN pharmacy_batches b ON d.id = b.drug_id
|
||||||
|
WHERE b.quantity > 0";
|
||||||
|
$total = $pdo->query($countSql)->fetchColumn();
|
||||||
|
|
||||||
|
$sql = "SELECT d.name_en as drug_name, d.name_ar as drug_name_ar,
|
||||||
|
g.name_en as category_name,
|
||||||
|
SUM(b.quantity) as stock_quantity,
|
||||||
|
SUM(b.quantity * b.cost_price) / SUM(b.quantity) as avg_cost,
|
||||||
|
SUM(b.quantity * b.sale_price) / SUM(b.quantity) as selling_price,
|
||||||
|
SUM(b.quantity * b.cost_price) as total_cost_value,
|
||||||
|
SUM(b.quantity * b.sale_price) as total_sales_value
|
||||||
|
FROM drugs d
|
||||||
|
JOIN pharmacy_batches b ON d.id = b.drug_id
|
||||||
|
LEFT JOIN drugs_groups g ON d.group_id = g.id
|
||||||
|
WHERE b.quantity > 0
|
||||||
|
GROUP BY d.id
|
||||||
|
ORDER BY d.name_en ASC
|
||||||
|
LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
// Calculate Grand Totals (entire stock)
|
||||||
|
$grandTotalSql = "SELECT SUM(b.quantity * b.cost_price) as total_cost,
|
||||||
|
SUM(b.quantity * b.sale_price) as total_sales
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
WHERE b.quantity > 0";
|
||||||
|
$grandTotals = $pdo->query($grandTotalSql)->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'grand_total_cost' => $grandTotals['total_cost'] ?? 0,
|
||||||
|
'grand_total_sales' => $grandTotals['total_sales'] ?? 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
} elseif ($type === 'sales') {
|
||||||
|
// Count
|
||||||
|
$countSql = "SELECT COUNT(*) FROM pharmacy_sales WHERE created_at BETWEEN ? AND ? + INTERVAL 1 DAY";
|
||||||
|
$countStmt = $pdo->prepare($countSql);
|
||||||
|
$countStmt->execute([$startDate, $endDate]);
|
||||||
|
$total = $countStmt->fetchColumn();
|
||||||
|
|
||||||
|
$sql = "SELECT s.id, s.created_at, s.total_amount, s.payment_method,
|
||||||
|
p.name as patient_name,
|
||||||
|
(SELECT COUNT(*) FROM pharmacy_sale_items i WHERE i.sale_id = s.id) as item_count
|
||||||
|
FROM pharmacy_sales s
|
||||||
|
LEFT JOIN patients p ON s.patient_id = p.id
|
||||||
|
WHERE s.created_at BETWEEN :start AND :end + INTERVAL 1 DAY
|
||||||
|
ORDER BY s.created_at DESC
|
||||||
|
LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
$grandTotalSql = "SELECT SUM(total_amount) as total FROM pharmacy_sales WHERE created_at BETWEEN ? AND ? + INTERVAL 1 DAY";
|
||||||
|
$grandTotalStmt = $pdo->prepare($grandTotalSql);
|
||||||
|
$grandTotalStmt->execute([$startDate, $endDate]);
|
||||||
|
$grandTotal = $grandTotalStmt->fetchColumn();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':start', $startDate);
|
||||||
|
$stmt->bindValue(':end', $endDate);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'grand_total_sales' => $grandTotal ?? 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
} elseif ($type === 'expiry') {
|
||||||
|
$countSql = "SELECT COUNT(*) FROM pharmacy_batches WHERE expiry_date BETWEEN ? AND ? AND quantity > 0";
|
||||||
|
$countStmt = $pdo->prepare($countSql);
|
||||||
|
$countStmt->execute([$startDate, $endDate]);
|
||||||
|
$total = $countStmt->fetchColumn();
|
||||||
|
|
||||||
|
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity,
|
||||||
|
d.name_en as drug_name, d.name_ar as drug_name_ar,
|
||||||
|
s.name_en as supplier_name,
|
||||||
|
DATEDIFF(b.expiry_date, CURDATE()) as days_remaining
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
JOIN drugs d ON b.drug_id = d.id
|
||||||
|
LEFT JOIN suppliers s ON b.supplier_id = s.id
|
||||||
|
WHERE b.expiry_date BETWEEN :start AND :end AND b.quantity > 0
|
||||||
|
ORDER BY b.expiry_date ASC
|
||||||
|
LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':start', $startDate);
|
||||||
|
$stmt->bindValue(':end', $endDate);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
]);
|
||||||
|
|
||||||
|
} elseif ($type === 'purchase_report') {
|
||||||
|
$countSql = "SELECT COUNT(*) FROM pharmacy_lpos WHERE lpo_date BETWEEN ? AND ?";
|
||||||
|
$countStmt = $pdo->prepare($countSql);
|
||||||
|
$countStmt->execute([$startDate, $endDate]);
|
||||||
|
$total = $countStmt->fetchColumn();
|
||||||
|
|
||||||
|
$sql = "SELECT l.id, l.lpo_date, l.status, l.total_amount, s.name_en as supplier_name, s.name_ar as supplier_name_ar
|
||||||
|
FROM pharmacy_lpos l
|
||||||
|
LEFT JOIN suppliers s ON l.supplier_id = s.id
|
||||||
|
WHERE l.lpo_date BETWEEN :start AND :end
|
||||||
|
ORDER BY l.lpo_date DESC
|
||||||
|
LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
$grandTotalSql = "SELECT SUM(total_amount) as total FROM pharmacy_lpos WHERE lpo_date BETWEEN ? AND ?";
|
||||||
|
$grandTotalStmt = $pdo->prepare($grandTotalSql);
|
||||||
|
$grandTotalStmt->execute([$startDate, $endDate]);
|
||||||
|
$grandTotal = $grandTotalStmt->fetchColumn();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':start', $startDate);
|
||||||
|
$stmt->bindValue(':end', $endDate);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'grand_total_purchases' => $grandTotal ?? 0
|
||||||
|
]);
|
||||||
|
} elseif ($type === 'low_stock') {
|
||||||
|
// Reuse get_low_stock logic
|
||||||
|
$countSql = "SELECT COUNT(*) FROM (
|
||||||
|
SELECT d.id
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
GROUP BY d.id
|
||||||
|
HAVING COALESCE(SUM(b.quantity), 0) <= MAX(d.reorder_level)
|
||||||
|
) as total";
|
||||||
|
$total = $pdo->query($countSql)->fetchColumn();
|
||||||
|
|
||||||
|
$sql = "SELECT d.id, d.name_en, d.name_ar, d.min_stock_level, d.reorder_level, d.unit,
|
||||||
|
COALESCE(SUM(b.quantity), 0) as total_stock
|
||||||
|
FROM drugs d
|
||||||
|
LEFT JOIN pharmacy_batches b ON d.id = b.drug_id AND b.quantity > 0 AND b.expiry_date >= CURDATE()
|
||||||
|
GROUP BY d.id
|
||||||
|
HAVING total_stock <= MAX(d.reorder_level)
|
||||||
|
ORDER BY total_stock ASC
|
||||||
|
LIMIT :limit OFFSET :offset";
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Exception("Invalid action");
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
282
api/pharmacy_lpo.php
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if ($action === 'create_lpo') {
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($data['supplier_id']) || empty($data['items'])) {
|
||||||
|
throw new Exception("Supplier and items are required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pharmacy_lpos (supplier_id, lpo_date, status, total_amount, notes) VALUES (?, ?, 'Draft', ?, ?)");
|
||||||
|
$stmt->execute([
|
||||||
|
$data['supplier_id'],
|
||||||
|
$data['lpo_date'] ?? date('Y-m-d'),
|
||||||
|
$data['total_amount'] ?? 0,
|
||||||
|
$data['notes'] ?? ''
|
||||||
|
]);
|
||||||
|
$lpoId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
$stmtItem = $pdo->prepare("INSERT INTO pharmacy_lpo_items (lpo_id, drug_id, quantity, cost_price, total_cost, batch_number, expiry_date) VALUES (?, ?, ?, ?, ?, ?, ?)");
|
||||||
|
|
||||||
|
foreach ($data['items'] as $item) {
|
||||||
|
$stmtItem->execute([
|
||||||
|
$lpoId,
|
||||||
|
$item['drug_id'],
|
||||||
|
$item['quantity'],
|
||||||
|
$item['cost_price'],
|
||||||
|
$item['total_cost'],
|
||||||
|
!empty($item['batch_number']) ? $item['batch_number'] : null,
|
||||||
|
!empty($item['expiry_date']) ? $item['expiry_date'] : null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Purchase created successfully']);
|
||||||
|
|
||||||
|
} elseif ($action === 'update_status') {
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
if (empty($data['id']) || empty($data['status'])) {
|
||||||
|
throw new Exception("ID and Status are required");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
// Check if items update is requested (specifically for Received status or corrections)
|
||||||
|
if (!empty($data['items']) && is_array($data['items'])) {
|
||||||
|
$updateItemStmt = $pdo->prepare("UPDATE pharmacy_lpo_items SET batch_number = ?, expiry_date = ? WHERE id = ?");
|
||||||
|
foreach ($data['items'] as $item) {
|
||||||
|
if (!empty($item['id'])) {
|
||||||
|
$updateItemStmt->execute([
|
||||||
|
!empty($item['batch_number']) ? $item['batch_number'] : null,
|
||||||
|
!empty($item['expiry_date']) ? $item['expiry_date'] : null,
|
||||||
|
$item['id']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If status is being changed to Received, we must update stock
|
||||||
|
if ($data['status'] === 'Received') {
|
||||||
|
// Fetch LPO items (re-fetch to get updated values)
|
||||||
|
$stmtItems = $pdo->prepare("SELECT * FROM pharmacy_lpo_items WHERE lpo_id = ?");
|
||||||
|
$stmtItems->execute([$data['id']]);
|
||||||
|
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Fetch LPO details for supplier
|
||||||
|
$stmtLPO = $pdo->prepare("SELECT supplier_id FROM pharmacy_lpos WHERE id = ?");
|
||||||
|
$stmtLPO->execute([$data['id']]);
|
||||||
|
$lpo = $stmtLPO->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$batchStmt = $pdo->prepare("INSERT INTO pharmacy_batches (drug_id, batch_number, expiry_date, quantity, cost_price, sale_price, supplier_id, received_date) VALUES (?, ?, ?, ?, ?, ?, ?, CURDATE())");
|
||||||
|
|
||||||
|
// We need sale price for the batch. Ideally, LPO should have it or we fetch current from drugs table.
|
||||||
|
$drugPriceStmt = $pdo->prepare("SELECT price FROM drugs WHERE id = ?");
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (empty($item['batch_number']) || empty($item['expiry_date'])) {
|
||||||
|
// If still missing (should be caught by UI), generate defaults
|
||||||
|
$item['batch_number'] = $item['batch_number'] ?? 'BATCH-' . date('Ymd') . '-' . $item['id'];
|
||||||
|
$item['expiry_date'] = $item['expiry_date'] ?? date('Y-m-d', strtotime('+1 year'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$drugPriceStmt->execute([$item['drug_id']]);
|
||||||
|
$drug = $drugPriceStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$salePrice = $drug['price'] ?? ($item['cost_price'] * 1.5); // Fallback margin
|
||||||
|
|
||||||
|
$batchStmt->execute([
|
||||||
|
$item['drug_id'],
|
||||||
|
$item['batch_number'],
|
||||||
|
$item['expiry_date'],
|
||||||
|
$item['quantity'],
|
||||||
|
$item['cost_price'],
|
||||||
|
$salePrice,
|
||||||
|
$lpo['supplier_id']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("UPDATE pharmacy_lpos SET status = ? WHERE id = ?");
|
||||||
|
$stmt->execute([$data['status'], $data['id']]);
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true]);
|
||||||
|
|
||||||
|
} elseif ($action === 'create_return') {
|
||||||
|
$data = json_decode(file_get_contents('php://input'), true);
|
||||||
|
|
||||||
|
if (empty($data['supplier_id']) || empty($data['items'])) {
|
||||||
|
throw new Exception("Supplier and items are required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO pharmacy_purchase_returns (supplier_id, return_date, total_amount, reason) VALUES (?, ?, ?, ?)");
|
||||||
|
$stmt->execute([
|
||||||
|
$data['supplier_id'],
|
||||||
|
$data['return_date'] ?? date('Y-m-d'),
|
||||||
|
$data['total_amount'] ?? 0,
|
||||||
|
$data['reason'] ?? ''
|
||||||
|
]);
|
||||||
|
$returnId = $pdo->lastInsertId();
|
||||||
|
|
||||||
|
$stmtItem = $pdo->prepare("INSERT INTO pharmacy_purchase_return_items (return_id, drug_id, batch_id, quantity, unit_price, total_price) VALUES (?, ?, ?, ?, ?, ?)");
|
||||||
|
$updateBatch = $pdo->prepare("UPDATE pharmacy_batches SET quantity = quantity - ? WHERE id = ?");
|
||||||
|
|
||||||
|
foreach ($data['items'] as $item) {
|
||||||
|
// Check stock first
|
||||||
|
$checkBatch = $pdo->prepare("SELECT quantity FROM pharmacy_batches WHERE id = ?");
|
||||||
|
$checkBatch->execute([$item['batch_id']]);
|
||||||
|
$currentStock = $checkBatch->fetchColumn();
|
||||||
|
|
||||||
|
if ($currentStock < $item['quantity']) {
|
||||||
|
throw new Exception("Insufficient stock in batch for drug ID " . $item['drug_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmtItem->execute([
|
||||||
|
$returnId,
|
||||||
|
$item['drug_id'],
|
||||||
|
$item['batch_id'],
|
||||||
|
$item['quantity'],
|
||||||
|
$item['unit_price'],
|
||||||
|
$item['total_price']
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Deduct stock
|
||||||
|
$updateBatch->execute([$item['quantity'], $item['batch_id']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$pdo->commit();
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Return created successfully']);
|
||||||
|
}
|
||||||
|
} elseif ($_SERVER['REQUEST_METHOD'] === 'GET') {
|
||||||
|
if ($action === 'get_lpos') {
|
||||||
|
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
// Count total
|
||||||
|
$countStmt = $pdo->query("SELECT COUNT(*) FROM pharmacy_lpos");
|
||||||
|
$total = $countStmt->fetchColumn();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT l.*, s.name_en as supplier_name
|
||||||
|
FROM pharmacy_lpos l
|
||||||
|
LEFT JOIN suppliers s ON l.supplier_id = s.id
|
||||||
|
ORDER BY l.created_at DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
");
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'pages' => ceil($total / $limit)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} elseif ($action === 'get_lpo_details') {
|
||||||
|
$id = $_GET['id'] ?? 0;
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT i.*, d.name_en as drug_name, d.sku
|
||||||
|
FROM pharmacy_lpo_items i
|
||||||
|
LEFT JOIN drugs d ON i.drug_id = d.id
|
||||||
|
WHERE i.lpo_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
|
||||||
|
} elseif ($action === 'get_suppliers') {
|
||||||
|
$stmt = $pdo->query("SELECT id, name_en, name_ar FROM suppliers ORDER BY name_en ASC");
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
|
||||||
|
} elseif ($action === 'get_drugs') {
|
||||||
|
$stmt = $pdo->query("SELECT id, name_en, name_ar, sku, price FROM drugs ORDER BY name_en ASC");
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
|
||||||
|
} elseif ($action === 'get_returns') {
|
||||||
|
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
|
||||||
|
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
|
||||||
|
$offset = ($page - 1) * $limit;
|
||||||
|
|
||||||
|
$countStmt = $pdo->query("SELECT COUNT(*) FROM pharmacy_purchase_returns");
|
||||||
|
$total = $countStmt->fetchColumn();
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT r.*, s.name_en as supplier_name
|
||||||
|
FROM pharmacy_purchase_returns r
|
||||||
|
LEFT JOIN suppliers s ON r.supplier_id = s.id
|
||||||
|
ORDER BY r.return_date DESC
|
||||||
|
LIMIT :limit OFFSET :offset
|
||||||
|
");
|
||||||
|
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
|
||||||
|
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'data' => $stmt->fetchAll(PDO::FETCH_ASSOC),
|
||||||
|
'total' => $total,
|
||||||
|
'page' => $page,
|
||||||
|
'limit' => $limit,
|
||||||
|
'pages' => ceil($total / $limit)
|
||||||
|
]);
|
||||||
|
|
||||||
|
} elseif ($action === 'get_return_details') {
|
||||||
|
$id = $_GET['id'] ?? 0;
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT i.*, d.name_en as drug_name, d.sku, b.batch_number
|
||||||
|
FROM pharmacy_purchase_return_items i
|
||||||
|
LEFT JOIN drugs d ON i.drug_id = d.id
|
||||||
|
LEFT JOIN pharmacy_batches b ON i.batch_id = b.id
|
||||||
|
WHERE i.return_id = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
|
||||||
|
} elseif ($action === 'get_supplier_batches') {
|
||||||
|
// Get batches for a specific supplier (or all if not specified, but usually filtered by drug)
|
||||||
|
// Ideally: We select a supplier for return, then we select items.
|
||||||
|
// Better: Select Drug -> Show Batches (maybe filter by supplier if we track supplier_id in batch)
|
||||||
|
$drug_id = $_GET['drug_id'] ?? 0;
|
||||||
|
$supplier_id = $_GET['supplier_id'] ?? 0;
|
||||||
|
|
||||||
|
$sql = "SELECT b.id, b.batch_number, b.expiry_date, b.quantity, b.cost_price
|
||||||
|
FROM pharmacy_batches b
|
||||||
|
WHERE b.drug_id = ? AND b.quantity > 0";
|
||||||
|
$params = [$drug_id];
|
||||||
|
|
||||||
|
if ($supplier_id) {
|
||||||
|
// If we want to strictly return only what we bought from this supplier:
|
||||||
|
// $sql .= " AND b.supplier_id = ?";
|
||||||
|
// $params[] = $supplier_id;
|
||||||
|
// BUT, sometimes we might return to a supplier what we bought elsewhere if they accept it,
|
||||||
|
// or `supplier_id` in batches might be null for old data.
|
||||||
|
// Let's NOT strictly enforce supplier_id match for now, just show all batches.
|
||||||
|
}
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
echo json_encode($stmt->fetchAll(PDO::FETCH_ASSOC));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode(['error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
251
api/queue.php
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
require_once __DIR__ . '/../includes/actions.php'; // For permissions if needed
|
||||||
|
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
$action = $_GET['action'] ?? '';
|
||||||
|
$lang = $_GET['lang'] ?? 'en';
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
// --- ADD TOKEN ---
|
||||||
|
if ($action === 'add') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
throw new Exception('Invalid request method');
|
||||||
|
}
|
||||||
|
|
||||||
|
$patient_id = $_POST['patient_id'] ?? null;
|
||||||
|
$department_id = $_POST['department_id'] ?? null;
|
||||||
|
$doctor_id = $_POST['doctor_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$patient_id || !$department_id) {
|
||||||
|
throw new Exception('Patient and Department are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get next token number for this department today
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT MAX(token_number)
|
||||||
|
FROM patient_queue
|
||||||
|
WHERE department_id = ?
|
||||||
|
AND DATE(created_at) = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$department_id, $today]);
|
||||||
|
$max_token = $stmt->fetchColumn();
|
||||||
|
$next_token = ($max_token) ? $max_token + 1 : 1;
|
||||||
|
|
||||||
|
// Insert
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO patient_queue (patient_id, department_id, doctor_id, token_number, status, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, 'waiting', NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([$patient_id, $department_id, $doctor_id ?: null, $next_token]);
|
||||||
|
$queue_id = $db->lastInsertId();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Token generated', 'token_number' => $next_token, 'queue_id' => $queue_id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LIST QUEUE ---
|
||||||
|
if ($action === 'list') {
|
||||||
|
$dept_id = $_GET['department_id'] ?? null;
|
||||||
|
$doc_id = $_GET['doctor_id'] ?? null;
|
||||||
|
$status = $_GET['status'] ?? null; // Can be comma separated 'waiting,serving'
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
|
||||||
|
$where = "WHERE DATE(q.created_at) = ?";
|
||||||
|
$params = [$today];
|
||||||
|
|
||||||
|
if ($dept_id) {
|
||||||
|
$where .= " AND q.department_id = ?";
|
||||||
|
$params[] = $dept_id;
|
||||||
|
}
|
||||||
|
if ($doc_id) {
|
||||||
|
$where .= " AND (q.doctor_id = ? OR q.doctor_id IS NULL)";
|
||||||
|
$params[] = $doc_id;
|
||||||
|
}
|
||||||
|
if ($status) {
|
||||||
|
$statuses = explode(',', $status);
|
||||||
|
$placeholders = implode(',', array_fill(0, count($statuses), '?'));
|
||||||
|
$where .= " AND q.status IN ($placeholders)";
|
||||||
|
$params = array_merge($params, $statuses);
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT q.*, td.name_$lang as target_department_name,
|
||||||
|
p.name as patient_name,
|
||||||
|
d.name_$lang as doctor_name,
|
||||||
|
d.name_en as doctor_name_en,
|
||||||
|
d.name_ar as doctor_name_ar,
|
||||||
|
d.room_number,
|
||||||
|
dept.name_$lang as department_name,
|
||||||
|
dept.name_en as department_name_en,
|
||||||
|
dept.name_ar as department_name_ar
|
||||||
|
FROM patient_queue q
|
||||||
|
JOIN patients p ON q.patient_id = p.id
|
||||||
|
JOIN departments dept ON q.department_id = dept.id
|
||||||
|
LEFT JOIN departments td ON q.target_department_id = td.id
|
||||||
|
LEFT JOIN employees d ON q.doctor_id = d.id
|
||||||
|
$where
|
||||||
|
ORDER BY
|
||||||
|
CASE WHEN q.status = 'serving' THEN 1 WHEN q.status = 'waiting' THEN 2 ELSE 3 END,
|
||||||
|
q.token_number ASC
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$queue = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $queue]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UPDATE STATUS ---
|
||||||
|
if ($action === 'update_status') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
throw new Exception('Invalid request method');
|
||||||
|
}
|
||||||
|
|
||||||
|
$queue_id = $_POST['queue_id'] ?? null;
|
||||||
|
$new_status = $_POST['status'] ?? null;
|
||||||
|
$doctor_id = $_POST['doctor_id'] ?? null; // If a doctor picks up a general department token
|
||||||
|
|
||||||
|
if (!$queue_id || !$new_status) {
|
||||||
|
throw new Exception('Queue ID and Status are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($new_status, ['waiting', 'serving', 'completed', 'cancelled'])) {
|
||||||
|
throw new Exception('Invalid status');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic: If setting to 'serving', update doctor_id if provided
|
||||||
|
$sql = "UPDATE patient_queue SET status = ?, updated_at = NOW()";
|
||||||
|
$params = [$new_status];
|
||||||
|
|
||||||
|
if ($new_status === 'serving' && $doctor_id) {
|
||||||
|
$sql .= ", doctor_id = ?";
|
||||||
|
$params[] = $doctor_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql .= " WHERE id = ?";
|
||||||
|
$params[] = $queue_id;
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Status updated']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- TRANSFER TOKEN ---
|
||||||
|
if ($action === 'transfer') {
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
throw new Exception('Invalid request method');
|
||||||
|
}
|
||||||
|
|
||||||
|
$queue_id = $_POST['queue_id'] ?? null;
|
||||||
|
$new_department_id = $_POST['department_id'] ?? null;
|
||||||
|
$new_doctor_id = $_POST['doctor_id'] ?? null;
|
||||||
|
|
||||||
|
if (!$queue_id || !$new_department_id) {
|
||||||
|
throw new Exception('Queue ID and Target Department are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current queue token
|
||||||
|
$stmt = $db->prepare("SELECT patient_id FROM patient_queue WHERE id = ?");
|
||||||
|
$stmt->execute([$queue_id]);
|
||||||
|
$current = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$current) {
|
||||||
|
throw new Exception('Queue token not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete the old token
|
||||||
|
$stmt = $db->prepare("UPDATE patient_queue SET status = 'completed', updated_at = NOW() WHERE id = ?");
|
||||||
|
$stmt->execute([$queue_id]);
|
||||||
|
|
||||||
|
// Create new token
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT MAX(token_number)
|
||||||
|
FROM patient_queue
|
||||||
|
WHERE department_id = ?
|
||||||
|
AND DATE(created_at) = ?
|
||||||
|
");
|
||||||
|
$stmt->execute([$new_department_id, $today]);
|
||||||
|
$max_token = $stmt->fetchColumn();
|
||||||
|
$next_token = ($max_token) ? $max_token + 1 : 1;
|
||||||
|
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
INSERT INTO patient_queue (patient_id, department_id, doctor_id, token_number, status, created_at)
|
||||||
|
VALUES (?, ?, ?, ?, 'waiting', NOW())
|
||||||
|
");
|
||||||
|
$stmt->execute([$current['patient_id'], $new_department_id, $new_doctor_id ?: null, $next_token]);
|
||||||
|
$new_queue_id = $db->lastInsertId();
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'message' => 'Token transferred', 'token_number' => $next_token, 'new_queue_id' => $new_queue_id]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SUMMARY ---
|
||||||
|
if ($action === 'summary') {
|
||||||
|
$today = date('Y-m-d');
|
||||||
|
$dept_id = $_GET['department_id'] ?? null;
|
||||||
|
|
||||||
|
$where = "WHERE DATE(q.created_at) = ?";
|
||||||
|
$params = [$today];
|
||||||
|
|
||||||
|
if ($dept_id) {
|
||||||
|
$where .= " AND q.department_id = ?";
|
||||||
|
$params[] = $dept_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sql = "
|
||||||
|
SELECT
|
||||||
|
dept.name_$lang as department_name,
|
||||||
|
dept.id as department_id,
|
||||||
|
SUM(CASE WHEN q.status = 'waiting' THEN 1 ELSE 0 END) as waiting,
|
||||||
|
SUM(CASE WHEN q.status = 'serving' THEN 1 ELSE 0 END) as serving,
|
||||||
|
SUM(CASE WHEN q.status = 'completed' THEN 1 ELSE 0 END) as completed
|
||||||
|
FROM patient_queue q
|
||||||
|
JOIN departments dept ON q.department_id = dept.id
|
||||||
|
LEFT JOIN departments td ON q.target_department_id = td.id
|
||||||
|
$where
|
||||||
|
GROUP BY dept.id
|
||||||
|
";
|
||||||
|
|
||||||
|
$stmt = $db->prepare($sql);
|
||||||
|
$stmt->execute($params);
|
||||||
|
$summary = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $summary]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GET ADS ---
|
||||||
|
if ($action === 'get_ads') {
|
||||||
|
$stmt = $db->query("SELECT * FROM queue_ads WHERE active = 1 ORDER BY created_at DESC");
|
||||||
|
$ads = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Return both languages
|
||||||
|
$data = array_map(function($ad) {
|
||||||
|
return [
|
||||||
|
'id' => $ad['id'],
|
||||||
|
'text_en' => $ad['text_en'],
|
||||||
|
'text_ar' => $ad['text_ar']
|
||||||
|
];
|
||||||
|
}, $ads);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'data' => $data]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception('Invalid action');
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
46
apply_migrations.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
// Ensure buffered query is on if possible (though config might override)
|
||||||
|
$db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
|
||||||
|
|
||||||
|
$db->query("SET FOREIGN_KEY_CHECKS=0;");
|
||||||
|
$files = glob('db/migrations/*.sql');
|
||||||
|
sort($files);
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
echo "Processing $file...\n";
|
||||||
|
$sql_content = file_get_contents($file);
|
||||||
|
$sql_content = preg_replace('/--.*$/m', '', $sql_content);
|
||||||
|
$statements = explode(';', $sql_content);
|
||||||
|
|
||||||
|
foreach ($statements as $sql) {
|
||||||
|
$sql = trim($sql);
|
||||||
|
if (empty($sql)) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use query() instead of exec() to handle potential result sets (like SELECT 1)
|
||||||
|
// and close the cursor explicitly.
|
||||||
|
$stmt = $db->query($sql);
|
||||||
|
if ($stmt) {
|
||||||
|
$stmt->closeCursor();
|
||||||
|
}
|
||||||
|
echo "Executed: " . substr(str_replace("\n", " ", $sql), 0, 60) . "...\n";
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$msg = $e->getMessage();
|
||||||
|
if (strpos($msg, "Duplicate column") !== false ||
|
||||||
|
strpos($msg, "already exists") !== false ||
|
||||||
|
strpos($msg, "Duplicate key") !== false ||
|
||||||
|
strpos($msg, "1062 Duplicate entry") !== false ||
|
||||||
|
strpos($msg, "1054 Unknown column") !== false ||
|
||||||
|
strpos($msg, "1146 Table") !== false ||
|
||||||
|
strpos($msg, "1553 Cannot drop index") !== false || strpos($msg, "1826 Duplicate FOREIGN KEY") !== false || strpos($msg, "1828 Cannot drop column") !== false) {
|
||||||
|
echo "Skipped (Exists): " . substr(str_replace("\n", " ", $sql), 0, 60) . "...\n";
|
||||||
|
} else {
|
||||||
|
echo "Error: " . $msg . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$db->query("SET FOREIGN_KEY_CHECKS=1;");
|
||||||
|
echo "All migrations applied.\n";
|
||||||
15
appointments.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'appointments';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
$db = db();
|
||||||
|
$lang = $_SESSION['lang'] ?? 'en';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/actions.php';
|
||||||
|
require_once __DIR__ . '/includes/common_data.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
require_once __DIR__ . '/includes/pages/appointments.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
BIN
assets/images/favicon_1774793140.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
assets/images/logo_1772630133.png
Normal file
|
After Width: | Height: | Size: 236 KiB |
BIN
assets/images/logo_1772630277.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
assets/images/logo_1774793140.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
83
assets/js/ai_helper.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
async function generateAISuggestion(button) {
|
||||||
|
const target = button.dataset.target || 'treatment_plan';
|
||||||
|
const modal = button.closest(".modal");
|
||||||
|
|
||||||
|
// Get editor instances or DOM elements
|
||||||
|
const symptomsEditor = $(modal.querySelector('textarea[name="symptoms"]'));
|
||||||
|
const diagnosisEditor = $(modal.querySelector('textarea[name="diagnosis"]'));
|
||||||
|
const treatmentPlanEditor = $(modal.querySelector('textarea[name="treatment_plan"]'));
|
||||||
|
|
||||||
|
// Helper to get value from Summernote or textarea
|
||||||
|
const getValue = (editor, selector) => {
|
||||||
|
if (editor.length && editor.summernote) return editor.summernote('code');
|
||||||
|
const el = modal.querySelector(selector);
|
||||||
|
return el ? el.value : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const symptomsText = getValue(symptomsEditor, 'textarea[name="symptoms"]');
|
||||||
|
const diagnosisText = getValue(diagnosisEditor, 'textarea[name="diagnosis"]');
|
||||||
|
|
||||||
|
// Helper to set value
|
||||||
|
const setValue = (editor, selector, val) => {
|
||||||
|
if (editor.length && editor.summernote) {
|
||||||
|
editor.summernote('code', val);
|
||||||
|
} else {
|
||||||
|
const el = modal.querySelector(selector);
|
||||||
|
if (el) el.value = val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
const cleanSymptoms = symptomsText.replace(/<[^>]*>/g, "").trim();
|
||||||
|
const cleanDiagnosis = diagnosisText.replace(/<[^>]*>/g, "").trim();
|
||||||
|
|
||||||
|
if (target === 'diagnosis' && !cleanSymptoms) {
|
||||||
|
alert("Please enter symptoms first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (target === 'treatment_plan' && (!cleanSymptoms && !cleanDiagnosis)) {
|
||||||
|
alert("Please enter symptoms or diagnosis first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalHTML = button.innerHTML;
|
||||||
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span> AI...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
target: target,
|
||||||
|
symptoms: symptomsText,
|
||||||
|
diagnosis: diagnosisText,
|
||||||
|
current_value: (target === 'symptoms' ? symptomsText : (target === 'diagnosis' ? diagnosisText : ''))
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await fetch("api/ai_report.php", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
if (target === 'symptoms') {
|
||||||
|
setValue(symptomsEditor, 'textarea[name="symptoms"]', data.report);
|
||||||
|
} else if (target === 'diagnosis') {
|
||||||
|
setValue(diagnosisEditor, 'textarea[name="diagnosis"]', data.report);
|
||||||
|
} else {
|
||||||
|
setValue(treatmentPlanEditor, 'textarea[name="treatment_plan"]', data.report);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert("AI Error: " + (data.error || "Unknown error"));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("AI Suggestion failed:", error);
|
||||||
|
alert("Failed to generate AI suggestion.");
|
||||||
|
} finally {
|
||||||
|
button.disabled = false;
|
||||||
|
button.innerHTML = originalHTML;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alias for backward compatibility
|
||||||
|
window.generateAIReport = generateAISuggestion;
|
||||||
@ -1,8 +1,10 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// --- Chat Widget Logic ---
|
||||||
const chatForm = document.getElementById('chat-form');
|
const chatForm = document.getElementById('chat-form');
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById('chat-input');
|
||||||
const chatMessages = document.getElementById('chat-messages');
|
const chatMessages = document.getElementById('chat-messages');
|
||||||
|
|
||||||
|
if (chatForm && chatInput && chatMessages) {
|
||||||
const appendMessage = (text, sender) => {
|
const appendMessage = (text, sender) => {
|
||||||
const msgDiv = document.createElement('div');
|
const msgDiv = document.createElement('div');
|
||||||
msgDiv.classList.add('message', sender);
|
msgDiv.classList.add('message', sender);
|
||||||
@ -36,4 +38,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Patient Form: Auto-calculate DOB from Age ---
|
||||||
|
// Use jQuery for better compatibility with Inputmask and existing events
|
||||||
|
function setupAgeToDob(ageId, dobId) {
|
||||||
|
$(document).on('input', '#' + ageId, function() {
|
||||||
|
var age = parseInt($(this).val());
|
||||||
|
var $dob = $('#' + dobId);
|
||||||
|
|
||||||
|
if (!isNaN(age) && age >= 0) {
|
||||||
|
var currentYear = new Date().getFullYear();
|
||||||
|
var birthYear = currentYear - age;
|
||||||
|
// Default to Jan 1st of the birth year: YYYY-01-01
|
||||||
|
var dob = birthYear + '-01-01';
|
||||||
|
|
||||||
|
// Set value and trigger input/change for Inputmask and other listeners
|
||||||
|
$dob.val(dob).trigger('input').trigger('change');
|
||||||
|
} else {
|
||||||
|
// Optional: Clear DOB if age is invalid/cleared?
|
||||||
|
// $dob.val('').trigger('input');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAgeToDob('add_patient_age', 'add_patient_dob');
|
||||||
|
setupAgeToDob('edit_patient_age', 'edit_patient_dob');
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
assets/uploads/users/user_1_1774149432.jpg
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 246 KiB |
BIN
assets/uploads/xrays/1773570971_ss.pdf
Normal file
BIN
assets/uploads/xrays/1773582607_69b6b90f692da_scale.jfif
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
assets/uploads/xrays/1773582628_69b6b9245162b_ورد1.jfif
Normal file
|
After Width: | Height: | Size: 12 KiB |
16
billing.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'billing';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$lang = $_SESSION['lang'];
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/actions.php';
|
||||||
|
require_once __DIR__ . '/includes/common_data.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
require_once __DIR__ . '/includes/pages/billing.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
6
check_appointments_schema.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
$stmt = $db->query("DESCRIBE appointments");
|
||||||
|
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
print_r($columns);
|
||||||
20
check_data.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
// Fetch one patient
|
||||||
|
$patient = $db->query("SELECT id, name FROM patients LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||||
|
// Fetch one doctor (employee with position 'Doctor')
|
||||||
|
$doctor = $db->query("
|
||||||
|
SELECT e.id, e.name_en
|
||||||
|
FROM employees e
|
||||||
|
JOIN positions p ON e.position_id = p.id
|
||||||
|
WHERE UPPER(p.name_en) = 'DOCTOR'
|
||||||
|
LIMIT 1")->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if ($patient && $doctor) {
|
||||||
|
echo "Found Patient: " . $patient['name'] . " (ID: " . $patient['id'] . ")\n";
|
||||||
|
echo "Found Doctor: " . $doctor['name_en'] . " (ID: " . $doctor['id'] . ")\n";
|
||||||
|
} else {
|
||||||
|
echo "Could not find patient or doctor.\n";
|
||||||
|
}
|
||||||
16
check_details_schema.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
$tables = ['xray_inquiry_items', 'inquiry_tests', 'visit_prescriptions', 'laboratory_inquiries'];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
try {
|
||||||
|
$stmt = $db->query("SHOW CREATE TABLE $table");
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
echo $row['Create Table'] . "\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Table $table not found: " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
19
check_doctor_holidays_table.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$result = $db->query("SHOW COLUMNS FROM doctor_holidays");
|
||||||
|
if ($result) {
|
||||||
|
$columns = $result->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
echo "Table 'doctor_holidays' exists with columns:\n";
|
||||||
|
foreach ($columns as $col) {
|
||||||
|
echo "- " . $col['Field'] . " (" . $col['Type'] . ")\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "Table 'doctor_holidays' does not exist.\n";
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error: " . $e->getMessage() . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
8
check_employees_schema.php
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
$stmt = $db->query("DESCRIBE employees");
|
||||||
|
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
echo "Columns in employees table:\n";
|
||||||
|
print_r($columns);
|
||||||
|
?>
|
||||||
6
check_nurses_schema.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
$stmt = $db->query("DESCRIBE nurses");
|
||||||
|
$columns = $stmt->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
print_r($columns);
|
||||||
24
check_pharmacy_schema.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
$tables = ['drugs', 'suppliers', 'visit_prescriptions'];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
echo "--- Table: $table ---
|
||||||
|
";
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->query("DESCRIBE $table");
|
||||||
|
$columns = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
foreach ($columns as $col) {
|
||||||
|
echo "{$col['Field']} ({$col['Type']})
|
||||||
|
";
|
||||||
|
}
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo "Error describing $table: " . $e->getMessage() . "
|
||||||
|
";
|
||||||
|
}
|
||||||
|
echo "
|
||||||
|
";
|
||||||
|
}
|
||||||
|
|
||||||
16
check_prices_schema.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
$tables = ['laboratory_tests', 'xray_tests', 'drugs'];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
try {
|
||||||
|
$stmt = $db->query("SHOW CREATE TABLE $table");
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
echo $row['Create Table'] . "\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Table $table not found or error: " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
16
check_schema_billing.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
$tables = ['services', 'xray_inquiries', 'laboratory_inquiries', 'visit_prescriptions', 'bill_items'];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
try {
|
||||||
|
$stmt = $db->query("SHOW CREATE TABLE $table");
|
||||||
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
echo $row['Create Table'] . "\n\n";
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo "Table $table not found or error: " . $e->getMessage() . "\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
16
cities.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'cities';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$lang = $_SESSION['lang'] ?? 'en';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/actions.php';
|
||||||
|
require_once __DIR__ . '/includes/common_data.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
require_once __DIR__ . '/includes/pages/cities.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
5
cookies.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Netscape HTTP Cookie File
|
||||||
|
# https://curl.se/docs/http-cookies.html
|
||||||
|
# This file was generated by libcurl! Edit at your own risk.
|
||||||
|
|
||||||
|
127.0.0.1 FALSE / FALSE 0 PHPSESSID hvh0shk5nt5h4nbcfvo601cpjc
|
||||||
42
dashboard.php
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
// Enable detailed error reporting for debugging 500 errors
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
ini_set('display_startup_errors', 1);
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
|
||||||
|
$section = 'dashboard';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
|
||||||
|
// Try to connect to DB first to catch connection errors early
|
||||||
|
try {
|
||||||
|
$db = db();
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
die("Database Connection Error: " . $e->getMessage());
|
||||||
|
} catch (Exception $e) {
|
||||||
|
die("General Error: " . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now include helpers, which can use the existing $db connection
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
// Auth Check
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
|
||||||
|
// $db is already set above, so no need to call db() again, but it's safe if we do.
|
||||||
|
// $db = db();
|
||||||
|
|
||||||
|
$lang = $_SESSION['lang'];
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/actions.php';
|
||||||
|
require_once __DIR__ . '/includes/common_data.php';
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax_search'])) {
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/pages/dashboard.php';
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax_search'])) {
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
|
}
|
||||||
@ -1,17 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
// Generated by setup_mariadb_project.sh — edit as needed.
|
// Generated by setup_mariadb_project.sh — edit as needed.
|
||||||
define('DB_HOST', '127.0.0.1');
|
if (!defined('DB_HOST')) define('DB_HOST', getenv('DB_HOST') ?: '127.0.0.1');
|
||||||
define('DB_NAME', 'app_38960');
|
if (!defined('DB_NAME')) define('DB_NAME', getenv('DB_NAME') ?: 'app_38960');
|
||||||
define('DB_USER', 'app_38960');
|
if (!defined('DB_USER')) define('DB_USER', getenv('DB_USER') ?: 'app_38960');
|
||||||
define('DB_PASS', '36fb441e-8408-4101-afdc-7911dc065e36');
|
if (!defined('DB_PASS')) define('DB_PASS', getenv('DB_PASS') ?: '36fb441e-8408-4101-afdc-7911dc065e36');
|
||||||
|
|
||||||
|
if (!function_exists('db')) {
|
||||||
function db() {
|
function db() {
|
||||||
static $pdo;
|
static $pdo;
|
||||||
if (!$pdo) {
|
if (!$pdo) {
|
||||||
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
$pdo = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset=utf8mb4', DB_USER, DB_PASS, [
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||||
|
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
return $pdo;
|
return $pdo;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
30
db/migrations/20260304_add_attachment_to_lab_tests.sql
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
-- Migration: Add attachment to inquiry_tests
|
||||||
|
-- This table might have been created by previous agents or manually
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS inquiry_tests (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
inquiry_id INT,
|
||||||
|
test_id INT,
|
||||||
|
result VARCHAR(255),
|
||||||
|
normal_range VARCHAR(255),
|
||||||
|
attachment VARCHAR(255),
|
||||||
|
FOREIGN KEY (inquiry_id) REFERENCES laboratory_inquiries(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (test_id) REFERENCES laboratory_tests(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Ensure attachment column exists (in case inquiry_tests already existed without it)
|
||||||
|
SET @dbname = DATABASE();
|
||||||
|
SET @tablename = "inquiry_tests";
|
||||||
|
SET @columnname = "attachment";
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE TABLE_SCHEMA = @dbname
|
||||||
|
AND TABLE_NAME = @tablename
|
||||||
|
AND COLUMN_NAME = @columnname
|
||||||
|
) > 0,
|
||||||
|
"SELECT 1",
|
||||||
|
"ALTER TABLE inquiry_tests ADD COLUMN attachment VARCHAR(255) AFTER normal_range"
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @preparedStatement;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
37
db/migrations/20260304_create_appointments_module.sql
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
-- Create appointments table
|
||||||
|
CREATE TABLE IF NOT EXISTS appointments (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
patient_id INT NOT NULL,
|
||||||
|
doctor_id INT NOT NULL,
|
||||||
|
start_time DATETIME NOT NULL,
|
||||||
|
end_time DATETIME NOT NULL,
|
||||||
|
status ENUM('Scheduled', 'Completed', 'Cancelled') DEFAULT 'Scheduled',
|
||||||
|
reason TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create holidays table
|
||||||
|
CREATE TABLE IF NOT EXISTS holidays (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
holiday_date DATE NOT NULL,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
is_recurring BOOLEAN DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create doctor schedules table (working hours)
|
||||||
|
CREATE TABLE IF NOT EXISTS doctor_schedules (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
doctor_id INT NOT NULL,
|
||||||
|
day_of_week INT NOT NULL, -- 0 (Sunday) to 6 (Saturday)
|
||||||
|
start_time TIME NOT NULL,
|
||||||
|
end_time TIME NOT NULL,
|
||||||
|
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Seed some holidays
|
||||||
|
INSERT INTO holidays (holiday_date, name_en, name_ar, is_recurring) VALUES
|
||||||
|
('2026-01-01', 'New Year', 'رأس السنة', TRUE),
|
||||||
|
('2026-12-25', 'Christmas', 'عيد الميلاد', TRUE);
|
||||||
18
db/migrations/20260304_create_settings_table.sql
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
setting_key VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
setting_value TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO settings (setting_key, setting_value) VALUES
|
||||||
|
('company_name', 'Hospital Management System'),
|
||||||
|
('company_logo', ''),
|
||||||
|
('company_favicon', ''),
|
||||||
|
('company_ctr_no', ''),
|
||||||
|
('company_registration_no', ''),
|
||||||
|
('company_address', ''),
|
||||||
|
('company_phone', ''),
|
||||||
|
('company_email', ''),
|
||||||
|
('company_vat_no', '');
|
||||||
38
db/migrations/20260304_create_xray_module.sql
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
-- X-Ray Module Tables
|
||||||
|
CREATE TABLE IF NOT EXISTS xray_groups (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS xray_tests (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
group_id INT,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (group_id) REFERENCES xray_groups(id) ON DELETE SET NULL
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS xray_inquiries (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
patient_name VARCHAR(255) NOT NULL,
|
||||||
|
inquiry_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
source ENUM('Internal', 'External') DEFAULT 'Internal',
|
||||||
|
status ENUM('Pending', 'Completed', 'Cancelled') DEFAULT 'Pending',
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS xray_inquiry_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
inquiry_id INT NOT NULL,
|
||||||
|
xray_id INT NOT NULL,
|
||||||
|
result TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (inquiry_id) REFERENCES xray_inquiries(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (xray_id) REFERENCES xray_tests(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
15
db/migrations/20260304_link_inquiries_to_visits.sql
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
-- Update laboratory_inquiries and xray_inquiries to link with patients and visits
|
||||||
|
ALTER TABLE laboratory_inquiries ADD COLUMN patient_id INT NULL AFTER id;
|
||||||
|
ALTER TABLE laboratory_inquiries ADD COLUMN visit_id INT NULL AFTER patient_id;
|
||||||
|
ALTER TABLE laboratory_inquiries MODIFY patient_name VARCHAR(255) NULL;
|
||||||
|
|
||||||
|
ALTER TABLE laboratory_inquiries ADD CONSTRAINT fk_lab_patient FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE laboratory_inquiries ADD CONSTRAINT fk_lab_visit FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE xray_inquiries ADD COLUMN patient_id INT NULL AFTER id;
|
||||||
|
ALTER TABLE xray_inquiries ADD COLUMN visit_id INT NULL AFTER patient_id;
|
||||||
|
ALTER TABLE xray_inquiries MODIFY patient_name VARCHAR(255) NULL;
|
||||||
|
|
||||||
|
ALTER TABLE xray_inquiries ADD CONSTRAINT fk_xray_patient FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE xray_inquiries ADD CONSTRAINT fk_xray_visit FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL;
|
||||||
49
db/migrations/20260304_seed_xray_data.sql
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
-- Seed X-Ray Groups
|
||||||
|
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Chest', 'الصدر');
|
||||||
|
SET @chest_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Extremities', 'الأطراف');
|
||||||
|
SET @ext_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Spine', 'العمود الفقري');
|
||||||
|
SET @spine_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Abdomen', 'البطن');
|
||||||
|
SET @abd_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Dental', 'الأسنان');
|
||||||
|
SET @den_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
INSERT INTO xray_groups (name_en, name_ar) VALUES ('Skull', 'الجمجمة');
|
||||||
|
SET @skl_id = LAST_INSERT_ID();
|
||||||
|
|
||||||
|
-- Seed X-Ray Tests
|
||||||
|
-- Chest
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@chest_id, 'Chest PA/Lateral', 'أشعة على الصدر - وضع أمامي جانبي', 150.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@chest_id, 'Chest PA Only', 'أشعة على الصدر - وضع أمامي خلفي فقط', 100.00);
|
||||||
|
|
||||||
|
-- Extremities
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Hand AP/Lateral', 'أشعة على اليد', 120.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Foot AP/Lateral', 'أشعة على القدم', 120.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Knee AP/Lateral', 'أشعة على الركبة', 140.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Shoulder AP/Lateral', 'أشعة على الكتف', 150.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Elbow AP/Lateral', 'أشعة على الكوع', 120.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@ext_id, 'Ankle AP/Lateral', 'أشعة على الكاحل', 120.00);
|
||||||
|
|
||||||
|
-- Spine
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@spine_id, 'Cervical Spine AP/Lateral', 'أشعة على الفقرات العنقية', 180.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@spine_id, 'Lumbar Spine AP/Lateral', 'أشعة على الفقرات القطنية', 200.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@spine_id, 'Thoracic Spine AP/Lateral', 'أشعة على الفقرات الصدرية', 180.00);
|
||||||
|
|
||||||
|
-- Abdomen
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@abd_id, 'Abdomen KUB', 'أشعة على البطن والمسالك البولية', 160.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@abd_id, 'Abdomen Erect/Supine', 'أشعة على البطن', 160.00);
|
||||||
|
|
||||||
|
-- Dental
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@den_id, 'Dental Panoramic', 'أشعة بانوراما للأسنان', 250.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@den_id, 'Periapical X-Ray', 'أشعة صغيرة للأسنان', 50.00);
|
||||||
|
|
||||||
|
-- Skull
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@skl_id, 'Skull AP/Lateral', 'أشعة على الجمجمة', 180.00);
|
||||||
|
INSERT INTO xray_tests (group_id, name_en, name_ar, price) VALUES (@skl_id, 'Paranasal Sinuses', 'أشعة على الجيوب الأنفية', 150.00);
|
||||||
2
db/migrations/20260304_z_add_attachment_to_xrays.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
-- Add attachment column to xray_inquiry_items to store uploaded images/results
|
||||||
|
ALTER TABLE xray_inquiry_items ADD COLUMN attachment VARCHAR(255) DEFAULT NULL;
|
||||||
5
db/migrations/20260305_add_details_to_patients.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
-- Add civil_id, nationality, city to patients
|
||||||
|
ALTER TABLE patients
|
||||||
|
ADD COLUMN civil_id VARCHAR(50) DEFAULT NULL,
|
||||||
|
ADD COLUMN nationality VARCHAR(100) DEFAULT NULL,
|
||||||
|
ADD COLUMN city VARCHAR(100) DEFAULT NULL;
|
||||||
20
db/migrations/20260305_create_drugs_tables.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS drugs_groups (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS drugs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
group_id INT,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
description_en TEXT,
|
||||||
|
description_ar TEXT,
|
||||||
|
default_dosage VARCHAR(255),
|
||||||
|
default_instructions TEXT,
|
||||||
|
price DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (group_id) REFERENCES drugs_groups(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
14
db/migrations/20260305_create_suppliers_module.sql
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS suppliers (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
contact_person VARCHAR(255),
|
||||||
|
phone VARCHAR(50),
|
||||||
|
email VARCHAR(100),
|
||||||
|
address TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE drugs ADD COLUMN expiry_date DATE DEFAULT NULL;
|
||||||
|
ALTER TABLE drugs ADD COLUMN supplier_id INT DEFAULT NULL;
|
||||||
|
ALTER TABLE drugs ADD CONSTRAINT fk_drugs_supplier FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL;
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS visit_prescriptions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
visit_id INT NOT NULL,
|
||||||
|
drug_name VARCHAR(255) NOT NULL,
|
||||||
|
dosage VARCHAR(100),
|
||||||
|
instructions TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
4
db/migrations/20260305_z_alter_drugs_name_length.sql
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE drugs MODIFY name_en TEXT;
|
||||||
|
ALTER TABLE drugs MODIFY name_ar TEXT;
|
||||||
|
ALTER TABLE drugs_groups MODIFY name_en TEXT;
|
||||||
|
ALTER TABLE drugs_groups MODIFY name_ar TEXT;
|
||||||
3
db/migrations/20260306_change_passion_to_position.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE employees ADD COLUMN IF NOT EXISTS position_id INT NULL;
|
||||||
|
ALTER TABLE employees DROP COLUMN IF EXISTS passion_en;
|
||||||
|
ALTER TABLE employees DROP COLUMN IF EXISTS passion_ar;
|
||||||
20
db/migrations/20260306_create_cities_table.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS cities (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name_en VARCHAR(100) NOT NULL,
|
||||||
|
name_ar VARCHAR(100) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
INSERT INTO cities (name_en, name_ar) VALUES
|
||||||
|
('Muscat', 'مسقط'),
|
||||||
|
('Salalah', 'صلالة'),
|
||||||
|
('Sohar', 'صحار'),
|
||||||
|
('Nizwa', 'نزوى'),
|
||||||
|
('Sur', 'صور'),
|
||||||
|
('Al Buraimi', 'البريمي'),
|
||||||
|
('Seeb', 'السيب'),
|
||||||
|
('Bawshar', 'بوشر'),
|
||||||
|
('Ibri', 'عبري'),
|
||||||
|
('Rustaq', 'الرستاق'),
|
||||||
|
('Khasab', 'خصب'),
|
||||||
|
('Bahla', 'بهلاء');
|
||||||
11
db/migrations/20260306_create_services_module.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS services (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
department_id INT NOT NULL,
|
||||||
|
price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
24
db/migrations/20260306_rename_poisons_to_positions.sql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
SET @dbname = DATABASE();
|
||||||
|
SET @tablename = "poisons";
|
||||||
|
SET @targetname = "positions";
|
||||||
|
|
||||||
|
SET @exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
|
||||||
|
WHERE table_schema = @dbname
|
||||||
|
AND table_name = @tablename
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @target_exists = (
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES
|
||||||
|
WHERE table_schema = @dbname
|
||||||
|
AND table_name = @targetname
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @stmt = IF(@exists > 0 AND @target_exists = 0,
|
||||||
|
CONCAT('RENAME TABLE ', @tablename, ' TO ', @targetname),
|
||||||
|
'SELECT 1'
|
||||||
|
);
|
||||||
|
|
||||||
|
PREPARE stmt FROM @stmt;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE xray_inquiry_items MODIFY COLUMN attachment LONGTEXT;
|
||||||
9
db/migrations/20260315_create_doctor_holidays.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS doctor_holidays (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
doctor_id INT NOT NULL,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
end_date DATE NOT NULL,
|
||||||
|
note VARCHAR(255) DEFAULT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
19
db/migrations/20260316_create_patient_queue.sql
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
-- Create patient_queue table
|
||||||
|
CREATE TABLE IF NOT EXISTS patient_queue (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
patient_id INT NOT NULL,
|
||||||
|
department_id INT NOT NULL,
|
||||||
|
doctor_id INT NULL,
|
||||||
|
visit_id INT NULL,
|
||||||
|
token_number INT NOT NULL,
|
||||||
|
status ENUM('waiting', 'serving', 'completed', 'cancelled') DEFAULT 'waiting',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (doctor_id) REFERENCES doctors(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Index for faster searching of today's queue
|
||||||
|
CREATE INDEX idx_queue_date_dept ON patient_queue(created_at, department_id);
|
||||||
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE departments ADD COLUMN show_in_queue BOOLEAN DEFAULT 1;
|
||||||
7
db/migrations/20260317_create_queue_ads.sql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS queue_ads (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
text_en TEXT NOT NULL,
|
||||||
|
text_ar TEXT NOT NULL,
|
||||||
|
active TINYINT(1) DEFAULT 1,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
11
db/migrations/20260317_enable_home_visits.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
-- Add columns to appointments table
|
||||||
|
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS nurse_id INT NULL;
|
||||||
|
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS visit_type ENUM('Clinic', 'Home') DEFAULT 'Clinic';
|
||||||
|
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS address TEXT NULL;
|
||||||
|
ALTER TABLE appointments ADD CONSTRAINT fk_appointment_nurse FOREIGN KEY (nurse_id) REFERENCES nurses(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Add columns to visits table
|
||||||
|
ALTER TABLE visits ADD COLUMN IF NOT EXISTS nurse_id INT NULL;
|
||||||
|
ALTER TABLE visits ADD COLUMN IF NOT EXISTS visit_type ENUM('Clinic', 'Home') DEFAULT 'Clinic';
|
||||||
|
ALTER TABLE visits ADD COLUMN IF NOT EXISTS address TEXT NULL;
|
||||||
|
ALTER TABLE visits ADD CONSTRAINT fk_visit_nurse FOREIGN KEY (nurse_id) REFERENCES nurses(id) ON DELETE SET NULL;
|
||||||
3
db/migrations/20260321_add_billing_columns.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE bills ADD COLUMN IF NOT EXISTS insurance_covered DECIMAL(10,2) DEFAULT 0.00;
|
||||||
|
ALTER TABLE bills ADD COLUMN IF NOT EXISTS patient_payable DECIMAL(10,2) DEFAULT 0.00;
|
||||||
|
ALTER TABLE bills ADD COLUMN IF NOT EXISTS total_amount DECIMAL(10,2) DEFAULT 0.00;
|
||||||
2
db/migrations/20260321_add_payment_method_to_bills.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE bills ADD COLUMN IF NOT EXISTS payment_method ENUM('Cash', 'Card', 'Insurance', 'Online', 'Other') DEFAULT 'Cash';
|
||||||
|
ALTER TABLE bills ADD COLUMN IF NOT EXISTS notes TEXT;
|
||||||
2
db/migrations/20260321_add_sku_to_drugs.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE drugs ADD COLUMN sku VARCHAR(50) DEFAULT NULL AFTER id;
|
||||||
|
CREATE INDEX idx_drugs_sku ON drugs(sku);
|
||||||
2
db/migrations/20260321_add_status_to_visits.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE visits ADD COLUMN IF NOT EXISTS status ENUM('CheckIn', 'In Progress', 'Completed', 'Cancelled') DEFAULT 'CheckIn';
|
||||||
|
ALTER TABLE visits ADD COLUMN IF NOT EXISTS checkout_time DATETIME NULL;
|
||||||
38
db/migrations/20260321_create_auth_system.sql
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
-- Create roles table
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
slug VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
permissions TEXT NULL, -- JSON or serialized array of permissions
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Create users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
role_id INT NOT NULL,
|
||||||
|
active TINYINT(1) DEFAULT 1,
|
||||||
|
last_login DATETIME NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE RESTRICT
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- Seed Roles
|
||||||
|
INSERT IGNORE INTO roles (name, slug, permissions) VALUES
|
||||||
|
('Administrator', 'admin', '*'),
|
||||||
|
('Doctor', 'doctor', '["dashboard", "patients", "visits", "appointments", "home_visits", "reports"]'),
|
||||||
|
('Nurse', 'nurse', '["dashboard", "patients", "visits", "queue"]'),
|
||||||
|
('Receptionist', 'receptionist', '["dashboard", "patients", "appointments", "queue", "billing"]'),
|
||||||
|
('Laboratorial', 'laboratorial', '["dashboard", "laboratory"]'),
|
||||||
|
('Radiologic', 'radiologic', '["dashboard", "xray"]');
|
||||||
|
|
||||||
|
-- Seed Default Admin User (password: admin123)
|
||||||
|
-- Using a simple hash for demonstration if PHP's password_hash is not available in SQL,
|
||||||
|
-- but ideally we should insert via PHP. For now, I will insert a placeholder and update it via PHP or assume I can use a known hash.
|
||||||
|
-- Hash for 'admin123' (bcrypt)
|
||||||
|
INSERT IGNORE INTO users (name, email, password, role_id)
|
||||||
|
SELECT 'System Admin', 'admin@hospital.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', id
|
||||||
|
FROM roles WHERE slug = 'admin' LIMIT 1;
|
||||||
10
db/migrations/20260321_create_insurance_payments.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS insurance_payments (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
insurance_company_id INT NOT NULL,
|
||||||
|
amount DECIMAL(10, 2) NOT NULL,
|
||||||
|
payment_date DATE NOT NULL,
|
||||||
|
reference_number VARCHAR(100) DEFAULT NULL,
|
||||||
|
notes TEXT DEFAULT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (insurance_company_id) REFERENCES insurance_companies(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
52
db/migrations/20260321_create_inventory_module.sql
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS inventory_categories (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS inventory_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
category_id INT,
|
||||||
|
name_en VARCHAR(255) NOT NULL,
|
||||||
|
name_ar VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
sku VARCHAR(100) UNIQUE,
|
||||||
|
unit VARCHAR(50) DEFAULT 'piece',
|
||||||
|
min_level INT DEFAULT 10,
|
||||||
|
reorder_level INT DEFAULT 20,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES inventory_categories(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS inventory_batches (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
item_id INT NOT NULL,
|
||||||
|
batch_number VARCHAR(100),
|
||||||
|
expiry_date DATE,
|
||||||
|
quantity INT NOT NULL DEFAULT 0,
|
||||||
|
cost_price DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
supplier_id INT,
|
||||||
|
received_date DATE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (item_id) REFERENCES inventory_items(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS inventory_transactions (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
item_id INT NOT NULL,
|
||||||
|
batch_id INT,
|
||||||
|
transaction_type ENUM('in', 'out', 'adjustment') NOT NULL,
|
||||||
|
quantity INT NOT NULL,
|
||||||
|
reference_type VARCHAR(50), -- 'purchase', 'consumption', 'manual_adjustment'
|
||||||
|
reference_id INT,
|
||||||
|
user_id INT,
|
||||||
|
transaction_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
notes TEXT,
|
||||||
|
FOREIGN KEY (item_id) REFERENCES inventory_items(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (batch_id) REFERENCES inventory_batches(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
21
db/migrations/20260321_create_pharmacy_lpo.sql
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS `pharmacy_lpos` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`supplier_id` INT NOT NULL,
|
||||||
|
`lpo_date` DATE NOT NULL,
|
||||||
|
`status` ENUM('Draft', 'Sent', 'Received', 'Cancelled') DEFAULT 'Draft',
|
||||||
|
`total_amount` DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
`notes` TEXT NULL,
|
||||||
|
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`supplier_id`) REFERENCES `suppliers`(`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `pharmacy_lpo_items` (
|
||||||
|
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`lpo_id` INT NOT NULL,
|
||||||
|
`drug_id` INT NOT NULL,
|
||||||
|
`quantity` INT NOT NULL,
|
||||||
|
`cost_price` DECIMAL(10, 2) NOT NULL,
|
||||||
|
`total_cost` DECIMAL(10, 2) NOT NULL,
|
||||||
|
FOREIGN KEY (`lpo_id`) REFERENCES `pharmacy_lpos`(`id`) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (`drug_id`) REFERENCES `drugs`(`id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
49
db/migrations/20260321_create_pharmacy_module.sql
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
-- Create pharmacy_batches table
|
||||||
|
CREATE TABLE IF NOT EXISTS pharmacy_batches (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
drug_id INT NOT NULL,
|
||||||
|
batch_number VARCHAR(50) NOT NULL,
|
||||||
|
expiry_date DATE NOT NULL,
|
||||||
|
quantity INT NOT NULL DEFAULT 0,
|
||||||
|
cost_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
sale_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
supplier_id INT NULL,
|
||||||
|
received_date DATE NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (drug_id) REFERENCES drugs(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (supplier_id) REFERENCES suppliers(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create pharmacy_sales table
|
||||||
|
CREATE TABLE IF NOT EXISTS pharmacy_sales (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
patient_id INT NULL,
|
||||||
|
visit_id INT NULL,
|
||||||
|
total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
payment_method VARCHAR(50) DEFAULT 'cash',
|
||||||
|
status VARCHAR(20) DEFAULT 'completed', -- completed, refunded, cancelled
|
||||||
|
notes TEXT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (patient_id) REFERENCES patients(id) ON DELETE SET NULL,
|
||||||
|
FOREIGN KEY (visit_id) REFERENCES visits(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create pharmacy_sale_items table
|
||||||
|
CREATE TABLE IF NOT EXISTS pharmacy_sale_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
sale_id INT NOT NULL,
|
||||||
|
drug_id INT NOT NULL,
|
||||||
|
batch_id INT NULL, -- Can be null if we track sales without specific batch selection (though we should enforce it for stock deduction)
|
||||||
|
quantity INT NOT NULL DEFAULT 1,
|
||||||
|
unit_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
total_price DECIMAL(10, 2) NOT NULL DEFAULT 0.00,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (sale_id) REFERENCES pharmacy_sales(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (drug_id) REFERENCES drugs(id),
|
||||||
|
FOREIGN KEY (batch_id) REFERENCES pharmacy_batches(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add stock management columns to drugs table
|
||||||
|
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS min_stock_level INT DEFAULT 10;
|
||||||
|
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS reorder_level INT DEFAULT 20;
|
||||||
|
ALTER TABLE drugs ADD COLUMN IF NOT EXISTS unit VARCHAR(50) DEFAULT 'pack';
|
||||||
29
db/migrations/20260321_create_purchase_returns.sql
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
-- Add batch and expiry to LPO items for receiving stock
|
||||||
|
ALTER TABLE pharmacy_lpo_items ADD COLUMN IF NOT EXISTS batch_number VARCHAR(50) NULL;
|
||||||
|
ALTER TABLE pharmacy_lpo_items ADD COLUMN IF NOT EXISTS expiry_date DATE NULL;
|
||||||
|
|
||||||
|
-- Create Purchase Returns table
|
||||||
|
CREATE TABLE IF NOT EXISTS pharmacy_purchase_returns (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
supplier_id INT NOT NULL,
|
||||||
|
return_date DATE NOT NULL,
|
||||||
|
total_amount DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
reason TEXT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Create Purchase Return Items table
|
||||||
|
CREATE TABLE IF NOT EXISTS pharmacy_purchase_return_items (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
return_id INT NOT NULL,
|
||||||
|
drug_id INT NOT NULL,
|
||||||
|
batch_id INT NULL, -- Which batch we are returning from
|
||||||
|
quantity INT NOT NULL,
|
||||||
|
unit_price DECIMAL(10, 2) NOT NULL, -- Refund price
|
||||||
|
total_price DECIMAL(10, 2) NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (return_id) REFERENCES pharmacy_purchase_returns(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (drug_id) REFERENCES drugs(id),
|
||||||
|
FOREIGN KEY (batch_id) REFERENCES pharmacy_batches(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE inventory_transactions ADD COLUMN IF NOT EXISTS department_id INT NULL;
|
||||||
|
ALTER TABLE inventory_transactions ADD CONSTRAINT fk_inventory_dept FOREIGN KEY (department_id) REFERENCES departments(id) ON DELETE SET NULL;
|
||||||
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE insurance_payments ADD COLUMN payment_method VARCHAR(50) DEFAULT 'Check' AFTER reference_number;
|
||||||
1
db/migrations/20260322_add_avatar_to_users.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE users ADD COLUMN avatar VARCHAR(255) DEFAULT NULL;
|
||||||
2
db/migrations/20260322_add_employee_to_nurses.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE nurses ADD COLUMN IF NOT EXISTS employee_id INT;
|
||||||
|
ALTER TABLE nurses ADD CONSTRAINT fk_nurse_employee FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE SET NULL;
|
||||||
1
db/migrations/20260322_add_image_to_drugs.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE drugs ADD COLUMN image VARCHAR(255) NULL;
|
||||||
1
db/migrations/20260322_add_nursing_notes_to_visits.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE visits ADD COLUMN IF NOT EXISTS nursing_notes TEXT AFTER temperature;
|
||||||
55
db/migrations/20260322_create_hr_module.sql
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
-- Add user_id to employees to link with login
|
||||||
|
ALTER TABLE employees ADD COLUMN IF NOT EXISTS user_id INT NULL;
|
||||||
|
ALTER TABLE employees ADD COLUMN IF NOT EXISTS join_date DATE NULL;
|
||||||
|
|
||||||
|
-- Attendance
|
||||||
|
CREATE TABLE IF NOT EXISTS attendance_logs (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
employee_id INT NOT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
check_in DATETIME NULL,
|
||||||
|
check_out DATETIME NULL,
|
||||||
|
status ENUM('Present', 'Late', 'Absent', 'On Leave') DEFAULT 'Present',
|
||||||
|
source VARCHAR(50) DEFAULT 'Web',
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Leaves
|
||||||
|
CREATE TABLE IF NOT EXISTS leave_requests (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
employee_id INT NOT NULL,
|
||||||
|
leave_type VARCHAR(50) NOT NULL,
|
||||||
|
start_date DATE NOT NULL,
|
||||||
|
end_date DATE NOT NULL,
|
||||||
|
days INT NOT NULL,
|
||||||
|
reason TEXT,
|
||||||
|
status ENUM('Pending', 'Approved', 'Rejected') DEFAULT 'Pending',
|
||||||
|
approved_by INT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Salaries / Payroll Info
|
||||||
|
CREATE TABLE IF NOT EXISTS employee_salaries (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
employee_id INT NOT NULL,
|
||||||
|
basic_salary DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
housing_allowance DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
transport_allowance DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
other_allowance DECIMAL(10, 2) DEFAULT 0.00,
|
||||||
|
currency VARCHAR(10) DEFAULT 'USD',
|
||||||
|
effective_date DATE NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (employee_id) REFERENCES employees(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Biometric Devices (for API auth)
|
||||||
|
CREATE TABLE IF NOT EXISTS biometric_devices (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
device_name VARCHAR(100) NOT NULL,
|
||||||
|
ip_address VARCHAR(50),
|
||||||
|
api_key VARCHAR(255) NOT NULL,
|
||||||
|
status TINYINT(1) DEFAULT 1,
|
||||||
|
last_seen DATETIME NULL
|
||||||
|
);
|
||||||
28
db/migrations/20260322_fix_appointments_start_time.sql
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
-- Fix appointments table schema: Add start_time/end_time if missing
|
||||||
|
-- This handles the case where init_db.php created the table with appointment_date instead of start_time
|
||||||
|
|
||||||
|
-- Add start_time if it doesn't exist
|
||||||
|
-- Note: MySQL/MariaDB < 10.2 don't support ADD COLUMN IF NOT EXISTS easily, so we use a stored procedure or just suppress errors in apply_migrations.php
|
||||||
|
-- Assuming MariaDB 10.2+ or suppression in apply_migrations.php for "Duplicate column"
|
||||||
|
|
||||||
|
ALTER TABLE appointments ADD COLUMN start_time DATETIME NULL;
|
||||||
|
ALTER TABLE appointments ADD COLUMN end_time DATETIME NULL;
|
||||||
|
|
||||||
|
-- Migrate data if appointment_date exists
|
||||||
|
-- Check if appointment_date exists first? SQL doesn't have conditional logic outside procedures easily.
|
||||||
|
-- We'll try to update, if column doesn't exist it will fail but that's fine if start_time is already populated.
|
||||||
|
-- Wait, if appointment_date doesn't exist, this query will fail and might stop migration script if not handled.
|
||||||
|
-- But apply_migrations.php continues on error? No, it catches exceptions per statement.
|
||||||
|
|
||||||
|
-- We can wrap in a procedure to be safe, but apply_migrations.php splits by ';'.
|
||||||
|
-- So let's just try the update. If appointment_date doesn't exist, it fails harmlessly.
|
||||||
|
|
||||||
|
UPDATE appointments SET start_time = appointment_date WHERE start_time IS NULL;
|
||||||
|
UPDATE appointments SET end_time = DATE_ADD(start_time, INTERVAL 30 MINUTE) WHERE end_time IS NULL AND start_time IS NOT NULL;
|
||||||
|
|
||||||
|
-- Make start_time NOT NULL after populating
|
||||||
|
ALTER TABLE appointments MODIFY COLUMN start_time DATETIME NOT NULL;
|
||||||
|
ALTER TABLE appointments MODIFY COLUMN end_time DATETIME NOT NULL;
|
||||||
|
|
||||||
|
-- Drop appointment_date if it exists (optional cleanup)
|
||||||
|
-- ALTER TABLE appointments DROP COLUMN appointment_date;
|
||||||
64
db/migrations/20260322_merge_doctors_nurses_into_hr.sql
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
-- Migration to merge Doctors and Nurses into HR (Employees)
|
||||||
|
-- Step 1: Add new columns to hold Employee IDs
|
||||||
|
ALTER TABLE visits ADD COLUMN IF NOT EXISTS doctor_employee_id INT NULL;
|
||||||
|
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS doctor_employee_id INT NULL;
|
||||||
|
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS nurse_employee_id INT NULL;
|
||||||
|
|
||||||
|
-- Step 2: Migrate data (if doctors/nurses have employee_id set)
|
||||||
|
-- Update Visits
|
||||||
|
UPDATE visits v
|
||||||
|
JOIN doctors d ON v.doctor_id = d.id
|
||||||
|
SET v.doctor_employee_id = d.employee_id
|
||||||
|
WHERE d.employee_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Update Appointments (Doctor)
|
||||||
|
UPDATE appointments a
|
||||||
|
JOIN doctors d ON a.doctor_id = d.id
|
||||||
|
SET a.doctor_employee_id = d.employee_id
|
||||||
|
WHERE d.employee_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Update Appointments (Nurse)
|
||||||
|
UPDATE appointments a
|
||||||
|
JOIN nurses n ON a.nurse_id = n.id
|
||||||
|
SET a.nurse_employee_id = n.employee_id
|
||||||
|
WHERE n.employee_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Step 3: Drop old Foreign Keys (Constraint names might vary, so we try standard names or rely on DROP COLUMN to drop FKs in some DBs, but explicitly dropping FK is safer)
|
||||||
|
-- Finding constraint names is hard in SQL script without dynamic SQL.
|
||||||
|
-- However, in MariaDB/MySQL, dropping the column usually drops the FK.
|
||||||
|
-- But to be safe, we will try to drop the standard named constraints if known, or just proceed with DROP COLUMN which should work if no other constraints block it.
|
||||||
|
|
||||||
|
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS visits_ibfk_2; -- doctor_id
|
||||||
|
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS fk_visit_nurse;
|
||||||
|
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS fk_visit_doctor_employee;
|
||||||
|
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS appointments_ibfk_2; -- doctor_id
|
||||||
|
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS appointments_ibfk_3; -- nurse_id
|
||||||
|
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appointment_nurse;
|
||||||
|
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appt_doctor_employee;
|
||||||
|
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appt_nurse_employee;
|
||||||
|
|
||||||
|
-- Also drop keys/indexes if they exist separate from FK
|
||||||
|
ALTER TABLE visits DROP KEY IF EXISTS doctor_id;
|
||||||
|
ALTER TABLE appointments DROP KEY IF EXISTS doctor_id;
|
||||||
|
ALTER TABLE appointments DROP KEY IF EXISTS nurse_id;
|
||||||
|
|
||||||
|
-- Step 4: Drop old columns
|
||||||
|
ALTER TABLE visits DROP COLUMN doctor_id;
|
||||||
|
ALTER TABLE appointments DROP COLUMN doctor_id;
|
||||||
|
ALTER TABLE appointments DROP COLUMN nurse_id;
|
||||||
|
|
||||||
|
-- Step 5: Rename new columns to match standard naming (or keep them and add FK)
|
||||||
|
-- Let's rename them back to doctor_id and nurse_id but now they point to employees
|
||||||
|
ALTER TABLE visits CHANGE COLUMN doctor_employee_id doctor_id INT NULL;
|
||||||
|
ALTER TABLE appointments CHANGE COLUMN doctor_employee_id doctor_id INT NULL;
|
||||||
|
ALTER TABLE appointments CHANGE COLUMN nurse_employee_id nurse_id INT NULL;
|
||||||
|
|
||||||
|
-- Step 6: Add new Foreign Keys to employees
|
||||||
|
ALTER TABLE visits ADD CONSTRAINT fk_visit_doctor_employee FOREIGN KEY (doctor_id) REFERENCES employees(id) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE appointments ADD CONSTRAINT fk_appt_doctor_employee FOREIGN KEY (doctor_id) REFERENCES employees(id) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE appointments ADD CONSTRAINT fk_appt_nurse_employee FOREIGN KEY (nurse_id) REFERENCES employees(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Step 7: Drop obsolete tables
|
||||||
|
DROP TABLE IF EXISTS doctor_holidays; -- If exists
|
||||||
|
DROP TABLE IF EXISTS doctors;
|
||||||
|
DROP TABLE IF EXISTS nurses;
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
-- Final cleanup for Doctors/Nurses migration
|
||||||
|
|
||||||
|
-- Fix Patient Queue Doctor ID (References doctors)
|
||||||
|
ALTER TABLE patient_queue ADD COLUMN IF NOT EXISTS doctor_employee_id INT NULL;
|
||||||
|
|
||||||
|
-- Migrate data
|
||||||
|
UPDATE patient_queue q
|
||||||
|
JOIN doctors d ON q.doctor_id = d.id
|
||||||
|
SET q.doctor_employee_id = d.employee_id
|
||||||
|
WHERE d.employee_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Drop old FK
|
||||||
|
ALTER TABLE patient_queue DROP FOREIGN KEY IF EXISTS patient_queue_ibfk_3;
|
||||||
|
|
||||||
|
-- Drop old column
|
||||||
|
ALTER TABLE patient_queue DROP COLUMN doctor_id;
|
||||||
|
|
||||||
|
-- Rename new column
|
||||||
|
ALTER TABLE patient_queue CHANGE COLUMN doctor_employee_id doctor_id INT NULL;
|
||||||
|
|
||||||
|
-- Add new FK to employees
|
||||||
|
ALTER TABLE patient_queue ADD CONSTRAINT fk_queue_doctor_employee FOREIGN KEY (doctor_id) REFERENCES employees(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Now drop doctors table
|
||||||
|
DROP TABLE IF EXISTS doctors;
|
||||||
58
db/migrations/20260322_merge_doctors_nurses_into_hr_fix.sql
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
-- Fix Migration: Merge Doctors and Nurses into HR (Employees) - Cleanup
|
||||||
|
|
||||||
|
-- 1. Drop dependent tables that reference doctors
|
||||||
|
DROP TABLE IF EXISTS doctor_schedules;
|
||||||
|
|
||||||
|
-- 2. Fix Visits Nurse ID
|
||||||
|
-- Add temp column if it doesn't exist (it wasn't added in previous migration)
|
||||||
|
ALTER TABLE visits ADD COLUMN IF NOT EXISTS nurse_employee_id INT NULL;
|
||||||
|
|
||||||
|
-- Migrate data from nurses table to visits.nurse_employee_id
|
||||||
|
UPDATE visits v
|
||||||
|
JOIN nurses n ON v.nurse_id = n.id
|
||||||
|
SET v.nurse_employee_id = n.employee_id
|
||||||
|
WHERE n.employee_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Drop old FK on visits.nurse_id
|
||||||
|
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS fk_visit_nurse;
|
||||||
|
|
||||||
|
-- Drop old column visits.nurse_id
|
||||||
|
-- We use a check to avoid error if it was already dropped (though unlikely)
|
||||||
|
ALTER TABLE visits DROP COLUMN nurse_id;
|
||||||
|
|
||||||
|
-- Rename new column to nurse_id
|
||||||
|
ALTER TABLE visits CHANGE COLUMN nurse_employee_id nurse_id INT NULL;
|
||||||
|
|
||||||
|
-- Add new FK to employees
|
||||||
|
ALTER TABLE visits DROP FOREIGN KEY IF EXISTS fk_visit_nurse_employee;
|
||||||
|
ALTER TABLE visits ADD CONSTRAINT fk_visit_nurse_employee FOREIGN KEY (nurse_id) REFERENCES employees(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
|
||||||
|
-- 3. Fix Appointments Nurse ID
|
||||||
|
-- Ensure nurse_employee_id exists (might have been created in previous migration)
|
||||||
|
ALTER TABLE appointments ADD COLUMN IF NOT EXISTS nurse_employee_id INT NULL;
|
||||||
|
|
||||||
|
-- Migrate data again just in case
|
||||||
|
UPDATE appointments a
|
||||||
|
JOIN nurses n ON a.nurse_id = n.id
|
||||||
|
SET a.nurse_employee_id = n.employee_id
|
||||||
|
WHERE n.employee_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- Drop old FK on appointments.nurse_id
|
||||||
|
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appointment_nurse;
|
||||||
|
|
||||||
|
-- Drop old column appointments.nurse_id
|
||||||
|
ALTER TABLE appointments DROP COLUMN nurse_id;
|
||||||
|
|
||||||
|
-- Rename new column to nurse_id
|
||||||
|
ALTER TABLE appointments CHANGE COLUMN nurse_employee_id nurse_id INT NULL;
|
||||||
|
|
||||||
|
-- Add new FK to employees
|
||||||
|
ALTER TABLE appointments DROP FOREIGN KEY IF EXISTS fk_appt_nurse_employee;
|
||||||
|
ALTER TABLE appointments ADD CONSTRAINT fk_appt_nurse_employee FOREIGN KEY (nurse_id) REFERENCES employees(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
|
||||||
|
-- 4. Drop obsolete tables
|
||||||
|
-- Now that FKs are gone, this should succeed.
|
||||||
|
DROP TABLE IF EXISTS doctors;
|
||||||
|
DROP TABLE IF EXISTS nurses;
|
||||||
1
db/migrations/20260323_add_active_to_departments.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE departments ADD COLUMN active BOOLEAN DEFAULT 1;
|
||||||
1
db/migrations/20260323_add_room_number_to_employees.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE employees ADD COLUMN room_number VARCHAR(50) NULL AFTER email;
|
||||||
3
db/migrations/20260328_add_smart_queue_routing.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE departments ADD COLUMN requires_vitals TINYINT(1) DEFAULT 0;
|
||||||
|
ALTER TABLE departments ADD COLUMN is_vitals_room TINYINT(1) DEFAULT 0;
|
||||||
|
ALTER TABLE patient_queue ADD COLUMN target_department_id INT(11) DEFAULT NULL;
|
||||||
12
db/migrations/20260328_z_seed_employees_if_empty.sql
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
-- Seed positions and employees if none exist (useful for fresh installs)
|
||||||
|
INSERT IGNORE INTO positions (id, name_en, name_ar, description_en, description_ar) VALUES
|
||||||
|
(1, 'Doctor', 'طبيب', 'Medical Doctor', 'طبيب بشري'),
|
||||||
|
(2, 'Nurse', 'ممرض', 'Registered Nurse', 'ممرض مسجل'),
|
||||||
|
(3, 'Receptionist', 'موظف استقبال', 'Front Desk', 'الاستقبال');
|
||||||
|
|
||||||
|
INSERT IGNORE INTO employees (id, name_en, name_ar, email, mobile, department_id, position_id, room_number) VALUES
|
||||||
|
(1, 'Dr. Ahmed Ali', 'د. أحمد علي', 'ahmed@hospital.com', '0501234567', 1, 1, '101'),
|
||||||
|
(2, 'Dr. Sarah Smith', 'د. سارة سميث', 'sarah@hospital.com', '0501234568', 2, 1, '102'),
|
||||||
|
(3, 'Dr. John Doe', 'د. جون دو', 'john@hospital.com', '0501234569', 4, 1, '103'),
|
||||||
|
(4, 'Nurse Fatima', 'الممرضة فاطمة', 'fatima@hospital.com', '0501234570', 3, 2, 'ER-1'),
|
||||||
|
(5, 'Nurse Mary', 'الممرضة ماري', 'mary@hospital.com', '0501234571', 1, 2, 'OPD-1');
|
||||||
53
db/migrations/20260329_add_sick_leave_to_visits.sql
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
SET @dbname = DATABASE();
|
||||||
|
SET @tablename = 'visits';
|
||||||
|
|
||||||
|
-- Add sick_leave_days
|
||||||
|
SET @columnname = 'sick_leave_days';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = @dbname
|
||||||
|
AND TABLE_NAME = @tablename
|
||||||
|
AND COLUMN_NAME = @columnname
|
||||||
|
) > 0,
|
||||||
|
"SELECT 1",
|
||||||
|
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN ", @columnname, " INT NULL;")
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
|
||||||
|
-- Add sick_leave_start_date
|
||||||
|
SET @columnname = 'sick_leave_start_date';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = @dbname
|
||||||
|
AND TABLE_NAME = @tablename
|
||||||
|
AND COLUMN_NAME = @columnname
|
||||||
|
) > 0,
|
||||||
|
"SELECT 1",
|
||||||
|
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN ", @columnname, " DATE NULL;")
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
|
|
||||||
|
-- Add sick_leave_remarks
|
||||||
|
SET @columnname = 'sick_leave_remarks';
|
||||||
|
SET @preparedStatement = (SELECT IF(
|
||||||
|
(
|
||||||
|
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||||
|
WHERE
|
||||||
|
TABLE_SCHEMA = @dbname
|
||||||
|
AND TABLE_NAME = @tablename
|
||||||
|
AND COLUMN_NAME = @columnname
|
||||||
|
) > 0,
|
||||||
|
"SELECT 1",
|
||||||
|
CONCAT("ALTER TABLE ", @tablename, " ADD COLUMN ", @columnname, " TEXT NULL;")
|
||||||
|
));
|
||||||
|
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||||
|
EXECUTE alterIfNotExists;
|
||||||
|
DEALLOCATE PREPARE alterIfNotExists;
|
||||||
37
debug_queue.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
require_once 'db/config.php';
|
||||||
|
$pdo = db();
|
||||||
|
|
||||||
|
echo "<h2>Patient Queue (Latest 5)</h2>";
|
||||||
|
$stmt = $pdo->query("SELECT * FROM patient_queue ORDER BY id DESC LIMIT 5");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if ($rows) {
|
||||||
|
echo "<table border='1'><tr>";
|
||||||
|
foreach (array_keys($rows[0]) as $k) echo "<th>$k</th>";
|
||||||
|
echo "</tr>";
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
echo "<tr>";
|
||||||
|
foreach ($row as $v) echo "<td>$v</td>";
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
echo "</table>";
|
||||||
|
} else {
|
||||||
|
echo "No records in patient_queue.<br>";
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "<h2>Visits (Latest 5)</h2>";
|
||||||
|
$stmt = $pdo->query("SELECT id, patient_id, doctor_id, created_at FROM visits ORDER BY id DESC LIMIT 5");
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
if ($rows) {
|
||||||
|
echo "<table border='1'><tr>";
|
||||||
|
foreach (array_keys($rows[0]) as $k) echo "<th>$k</th>";
|
||||||
|
echo "</tr>";
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
echo "<tr>";
|
||||||
|
foreach ($row as $v) echo "<td>$v</td>";
|
||||||
|
echo "</tr>";
|
||||||
|
}
|
||||||
|
echo "</table>";
|
||||||
|
} else {
|
||||||
|
echo "No records in visits.<br>";
|
||||||
|
}
|
||||||
10
delete_test_appointment.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
$db = db();
|
||||||
|
// Delete the test appointment created by test_create_appointment_curl.php (ID 14)
|
||||||
|
// Use a safe check to only delete if it looks like a test
|
||||||
|
$id = 14;
|
||||||
|
$stmt = $db->prepare("DELETE FROM appointments WHERE id = ? AND reason = 'Test API'");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
echo "Deleted test appointment $id (if it matched).\n";
|
||||||
|
|
||||||
12
delete_test_appointment_cleanup.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$id = 15;
|
||||||
|
|
||||||
|
$stmt = $db->prepare("DELETE FROM appointments WHERE id = ?");
|
||||||
|
$stmt->execute([$id]);
|
||||||
|
|
||||||
|
echo "Deleted appointment $id\n";
|
||||||
|
|
||||||
16
departments.php
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'departments';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$lang = $_SESSION['lang'];
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/actions.php';
|
||||||
|
require_once __DIR__ . '/includes/common_data.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
require_once __DIR__ . '/includes/pages/departments.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
35
download_patient_template.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
header("Location: login.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: text/csv; charset=utf-8');
|
||||||
|
header('Content-Disposition: attachment; filename="patients_import_template.csv"');
|
||||||
|
|
||||||
|
// Output UTF-8 BOM for Excel
|
||||||
|
echo "\xEF\xBB\xBF";
|
||||||
|
|
||||||
|
$output = fopen('php://output', 'w');
|
||||||
|
|
||||||
|
// Header row
|
||||||
|
fputcsv($output, [
|
||||||
|
'Patient Name',
|
||||||
|
'DOB',
|
||||||
|
'Nationality',
|
||||||
|
'Telephone',
|
||||||
|
'City'
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Sample row
|
||||||
|
fputcsv($output, [
|
||||||
|
'John Doe',
|
||||||
|
'1990-01-01',
|
||||||
|
'Omani',
|
||||||
|
'96812345678',
|
||||||
|
'Muscat'
|
||||||
|
]);
|
||||||
|
|
||||||
|
fclose($output);
|
||||||
|
exit;
|
||||||
22
drugs.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'drugs';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
$db = db();
|
||||||
|
$lang = $_SESSION['lang'] ?? 'en';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/actions.php';
|
||||||
|
require_once __DIR__ . '/includes/common_data.php';
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax_search'])) {
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/pages/drugs.php';
|
||||||
|
|
||||||
|
if (!isset($_GET['ajax_search'])) {
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
|
}
|
||||||
24
drugs_groups.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
$section = 'drugs_groups';
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
$db = db();
|
||||||
|
$lang = $_SESSION['lang'] ?? 'en';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/actions.php';
|
||||||
|
require_once __DIR__ . '/includes/common_data.php';
|
||||||
|
|
||||||
|
$is_ajax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest');
|
||||||
|
|
||||||
|
if (!$is_ajax) {
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/pages/drugs_groups.php';
|
||||||
|
|
||||||
|
if (!$is_ajax) {
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
|
}
|
||||||
26
employees.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
session_start();
|
||||||
|
if (!isset($_SESSION['lang'])) {
|
||||||
|
$_SESSION['lang'] = 'en';
|
||||||
|
}
|
||||||
|
if (isset($_GET['lang'])) {
|
||||||
|
$_SESSION['lang'] = $_GET['lang'] === 'ar' ? 'ar' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once 'db/config.php';
|
||||||
|
require_once 'lang.php';
|
||||||
|
require_once 'helpers.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
$lang = $_SESSION['lang'];
|
||||||
|
$section = 'employees';
|
||||||
|
|
||||||
|
require_once 'includes/actions.php';
|
||||||
|
require_once 'includes/common_data.php';
|
||||||
|
|
||||||
|
include 'includes/layout/header.php';
|
||||||
|
include 'includes/pages/employees.php';
|
||||||
|
include 'includes/layout/footer.php';
|
||||||
132
helpers.php
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
$SYSTEM_SETTINGS = null;
|
||||||
|
|
||||||
|
function get_system_settings() {
|
||||||
|
global $db, $SYSTEM_SETTINGS;
|
||||||
|
|
||||||
|
if ($SYSTEM_SETTINGS !== null) {
|
||||||
|
return $SYSTEM_SETTINGS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($db)) {
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
try {
|
||||||
|
$local_db = db();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If DB connection fails, return empty settings instead of crashing
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$local_db = $db;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stmt = $local_db->query('SELECT setting_key, setting_value FROM settings');
|
||||||
|
$settings = [];
|
||||||
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
$settings[$row['setting_key']] = $row['setting_value'];
|
||||||
|
}
|
||||||
|
$SYSTEM_SETTINGS = $settings;
|
||||||
|
return $settings;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function apply_timezone() {
|
||||||
|
try {
|
||||||
|
$s = get_system_settings();
|
||||||
|
if (!empty($s['timezone'])) {
|
||||||
|
date_default_timezone_set($s['timezone']);
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// Ignore timezone errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
apply_timezone();
|
||||||
|
|
||||||
|
function format_currency($amount) {
|
||||||
|
$settings = get_system_settings();
|
||||||
|
$currency_symbol = $settings['currency_symbol'] ?? '$';
|
||||||
|
$decimal_digits = isset($settings['decimal_digits']) ? (int)$settings['decimal_digits'] : 2;
|
||||||
|
|
||||||
|
return $currency_symbol . ' ' . number_format((float)$amount, $decimal_digits);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only start session if not already started
|
||||||
|
if (session_status() === PHP_SESSION_NONE) {
|
||||||
|
session_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once __DIR__ . '/lang.php';
|
||||||
|
|
||||||
|
if (!isset($_SESSION['lang'])) {
|
||||||
|
$_SESSION['lang'] = 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($_GET['lang'])) {
|
||||||
|
if ($_GET['lang'] === 'ar' || $_GET['lang'] === 'en') {
|
||||||
|
$_SESSION['lang'] = $_GET['lang'];
|
||||||
|
// Redirect to remove lang param
|
||||||
|
if (!headers_sent()) {
|
||||||
|
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function __($key) {
|
||||||
|
global $translations;
|
||||||
|
$lang = $_SESSION['lang'] ?? 'en'; // Fallback if session is empty
|
||||||
|
return $translations[$lang][$key] ?? $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function is_rtl() {
|
||||||
|
return ($_SESSION['lang'] ?? 'en') === 'ar';
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_dir() {
|
||||||
|
return is_rtl() ? 'rtl' : 'ltr';
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_lang_name() {
|
||||||
|
return ($_SESSION['lang'] ?? 'en') === 'ar' ? 'English' : 'العربية';
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_lang_code() {
|
||||||
|
return $_SESSION['lang'] ?? 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculate_age($dob) {
|
||||||
|
if (empty($dob)) return '-';
|
||||||
|
try {
|
||||||
|
$birthDate = new DateTime($dob);
|
||||||
|
$today = new DateTime('today');
|
||||||
|
return $birthDate->diff($today)->y;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!function_exists('mb_strimwidth')) {
|
||||||
|
function mb_strimwidth($string, $start, $width, $trimmarker = '...', $encoding = null) {
|
||||||
|
// Simple polyfill using substr
|
||||||
|
// 1. Handle start offset
|
||||||
|
$string = (string)$string;
|
||||||
|
if ($start > 0) {
|
||||||
|
$string = substr($string, $start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check length
|
||||||
|
if (strlen($string) <= $width) {
|
||||||
|
return $string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Truncate
|
||||||
|
$targetLen = $width - strlen($trimmarker);
|
||||||
|
if ($targetLen < 0) $targetLen = 0;
|
||||||
|
|
||||||
|
return substr($string, 0, $targetLen) . $trimmarker;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
home_visits.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
|
||||||
|
// Check for user role if needed (e.g. only doctors/admins/nurses)
|
||||||
|
// For now, accessible to all logged in users
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/pages/home_visits.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||
|
?>
|
||||||
60
hospital_services.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
require_once __DIR__ . '/db/config.php';
|
||||||
|
require_once __DIR__ . '/helpers.php';
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/auth.php';
|
||||||
|
check_auth();
|
||||||
|
|
||||||
|
$db = db();
|
||||||
|
|
||||||
|
// Handle Form Submissions
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
|
if (isset($_POST['action'])) {
|
||||||
|
try {
|
||||||
|
if ($_POST['action'] === 'add_service') {
|
||||||
|
$stmt = $db->prepare("INSERT INTO services (name_en, name_ar, department_id, price, is_active) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
$stmt->execute([
|
||||||
|
$_POST['name_en'],
|
||||||
|
$_POST['name_ar'],
|
||||||
|
$_POST['department_id'],
|
||||||
|
$_POST['price'],
|
||||||
|
isset($_POST['is_active']) ? 1 : 0
|
||||||
|
]);
|
||||||
|
$_SESSION['flash_message'] = '<div class="alert alert-success">' . __('service_added_successfully') . '</div>';
|
||||||
|
} elseif ($_POST['action'] === 'edit_service') {
|
||||||
|
$stmt = $db->prepare("UPDATE services SET name_en = ?, name_ar = ?, department_id = ?, price = ?, is_active = ? WHERE id = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
$_POST['name_en'],
|
||||||
|
$_POST['name_ar'],
|
||||||
|
$_POST['department_id'],
|
||||||
|
$_POST['price'],
|
||||||
|
isset($_POST['is_active']) ? 1 : 0,
|
||||||
|
$_POST['id']
|
||||||
|
]);
|
||||||
|
$_SESSION['flash_message'] = '<div class="alert alert-success">' . __('service_updated_successfully') . '</div>';
|
||||||
|
} elseif ($_POST['action'] === 'delete_service') {
|
||||||
|
$stmt = $db->prepare("DELETE FROM services WHERE id = ?");
|
||||||
|
$stmt->execute([$_POST['id']]);
|
||||||
|
$_SESSION['flash_message'] = '<div class="alert alert-success">' . __('service_deleted_successfully') . '</div>';
|
||||||
|
}
|
||||||
|
// Redirect after successful operation
|
||||||
|
header("Location: hospital_services.php");
|
||||||
|
exit;
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
$_SESSION['flash_message'] = '<div class="alert alert-danger">' . __('error') . ': ' . $e->getMessage() . '</div>';
|
||||||
|
// Redirect even on error, so the user sees the message
|
||||||
|
header("Location: hospital_services.php");
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session check logic (if needed in future)
|
||||||
|
// if (!isset($_SESSION['user_id'])) { ... }
|
||||||
|
|
||||||
|
$section = 'services';
|
||||||
|
$title = __('services');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/includes/layout/header.php';
|
||||||
|
require_once __DIR__ . '/includes/pages/services.php';
|
||||||
|
require_once __DIR__ . '/includes/layout/footer.php';
|
||||||