Add interactive OPD, IPD, and diagnosis dashboards with historical date ranges, KPI summaries, doctor and department workload, referral breakdowns, and diagnostic workflow statuses.
985 lines
38 KiB
PHP
985 lines
38 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use CodeIgniter\Shield\Models\UserModel;
|
|
|
|
class Home extends BaseController
|
|
{
|
|
private const USER_PROFILE_PHOTO_DIR = 'assets/images/user_profile';
|
|
|
|
private function ensureAuthenticated()
|
|
{
|
|
if (function_exists('auth') && auth()->loggedIn()) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(401)->setBody('Unauthorized');
|
|
}
|
|
|
|
return redirect()->to(base_url('login'));
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
$authRedirect = $this->ensureAuthenticated();
|
|
if ($authRedirect !== null) {
|
|
return $authRedirect;
|
|
}
|
|
|
|
return view('welcome_message', [
|
|
'initial_route' => base_url('dashboard'),
|
|
'initial_title' => 'Dashboard',
|
|
]);
|
|
}
|
|
|
|
public function billing()
|
|
{
|
|
$authRedirect = $this->ensureAuthenticated();
|
|
if ($authRedirect !== null) {
|
|
return $authRedirect;
|
|
}
|
|
|
|
$user = service('auth')->user();
|
|
|
|
return view('billing/main', ['user' => $user]);
|
|
}
|
|
|
|
public function dashboard()
|
|
{
|
|
$authRedirect = $this->ensureAuthenticated();
|
|
if ($authRedirect !== null) {
|
|
return $authRedirect;
|
|
}
|
|
|
|
if (! $this->request->isAJAX()) {
|
|
return view('welcome_message', [
|
|
'initial_route' => base_url('dashboard'),
|
|
'initial_title' => 'Dashboard',
|
|
]);
|
|
}
|
|
|
|
$user = service('auth')->user();
|
|
$today = date('Y-m-d');
|
|
$monthStart = date('Y-m-01');
|
|
$todayStart = $today . ' 00:00:00';
|
|
$tomorrowStart = date('Y-m-d', strtotime('+1 day')) . ' 00:00:00';
|
|
$monthStartDateTime = $monthStart . ' 00:00:00';
|
|
|
|
$opdToday = $this->db->table('opd_master')
|
|
->where('apointment_date >=', $todayStart)
|
|
->where('apointment_date <', $tomorrowStart)
|
|
->countAllResults();
|
|
|
|
$opdLast7Days = $this->db->table('opd_master')
|
|
->where('apointment_date >=', date('Y-m-d', strtotime('-7 days')) . ' 00:00:00')
|
|
->where('apointment_date <', $tomorrowStart)
|
|
->countAllResults();
|
|
|
|
$admitToday = $this->db->table('ipd_master')
|
|
->where('register_date', $today)
|
|
->countAllResults();
|
|
|
|
$dischargeToday = $this->db->table('ipd_master')
|
|
->where('discharge_date', $today)
|
|
->where('ipd_status', 1)
|
|
->countAllResults();
|
|
|
|
$currentIpd = $this->db->table('ipd_master')
|
|
->where('ipd_status', 0)
|
|
->countAllResults();
|
|
|
|
$currentOrgIpd = $this->db->table('ipd_master i')
|
|
->join('organization_case_master o', 'o.id = i.case_id', 'inner')
|
|
->where('o.case_type', 1)
|
|
->where('i.ipd_status', 0)
|
|
->countAllResults();
|
|
|
|
$diagnosisToday = $this->db->table('invoice_item ii')
|
|
->join('invoice_master im', 'im.id = ii.inv_master_id', 'inner')
|
|
->whereIn('ii.item_type', [1, 2, 3, 4, 5, 6, 30])
|
|
->where('im.invoice_status', 1)
|
|
->where('im.inv_date >=', $today)
|
|
->where('im.inv_date <', date('Y-m-d', strtotime('+1 day')))
|
|
->countAllResults();
|
|
|
|
$diagnosisCompletedToday = $this->db->table('invoice_item ii')
|
|
->join('invoice_master im', 'im.id = ii.inv_master_id', 'inner')
|
|
->join('lab_request lr', 'lr.charge_item_id = ii.id AND lr.charge_id = im.id', 'left')
|
|
->whereIn('ii.item_type', [1, 2, 3, 4, 5, 6, 30])
|
|
->where('im.invoice_status', 1)
|
|
->where('im.inv_date >=', $today)
|
|
->where('im.inv_date <', date('Y-m-d', strtotime('+1 day')))
|
|
->where('lr.status', 2)
|
|
->countAllResults();
|
|
|
|
$opdOrgList = $this->db->table('opd_master o')
|
|
->select('ins.short_name as org_name')
|
|
->select('COUNT(o.opd_id) as total_cases', false)
|
|
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'left')
|
|
->where('o.apointment_date >=', $monthStartDateTime)
|
|
->where('o.apointment_date <', $tomorrowStart)
|
|
->groupBy('ins.short_name')
|
|
->orderBy('total_cases', 'DESC')
|
|
->limit(10)
|
|
->get()
|
|
->getResult();
|
|
|
|
$opdDoctorList = $this->db->table('opd_master o')
|
|
->select('o.doc_name')
|
|
->select('COUNT(o.opd_id) as total_cases', false)
|
|
->select("SUM(CASE WHEN (o.insurance_id IS NULL OR o.insurance_id <= 1) THEN 1 ELSE 0 END) as direct_cases", false)
|
|
->select("SUM(CASE WHEN (o.insurance_id IS NULL OR o.insurance_id <= 1) THEN 0 ELSE 1 END) as org_cases", false)
|
|
->where('o.apointment_date >=', $todayStart)
|
|
->where('o.apointment_date <', $tomorrowStart)
|
|
->where('o.doc_name IS NOT NULL', null, false)
|
|
->where("o.doc_name != ''", null, false)
|
|
->groupBy('o.doc_name')
|
|
->orderBy('total_cases', 'DESC')
|
|
->limit(12)
|
|
->get()
|
|
->getResult();
|
|
|
|
$ipdDoctorList = $this->db->table('ipd_master_doc_list l')
|
|
->select('d.p_fname as doc_name')
|
|
->select('COUNT(DISTINCT l.ipd_id) as total_cases', false)
|
|
->join('doctor_master d', 'd.id = l.doc_id', 'inner')
|
|
->join('ipd_master i', 'i.id = l.ipd_id', 'inner')
|
|
->where('i.ipd_status', 0)
|
|
->groupBy('d.p_fname')
|
|
->orderBy('total_cases', 'DESC')
|
|
->limit(12)
|
|
->get()
|
|
->getResult();
|
|
|
|
$ipdOrgList = $this->db->table('organization_case_master o')
|
|
->select('ins.short_name as org_name')
|
|
->select('COUNT(o.id) as total_cases', false)
|
|
->join('ipd_master i', 'o.id = i.case_id', 'inner')
|
|
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'left')
|
|
->where('o.case_type', 1)
|
|
->where('i.ipd_status', 0)
|
|
->groupBy('ins.short_name')
|
|
->orderBy('total_cases', 'DESC')
|
|
->limit(10)
|
|
->get()
|
|
->getResult();
|
|
|
|
$trendStart = $monthStart; // First day of current month
|
|
$trendEnd = $today;
|
|
$trendDays = [];
|
|
|
|
// Generate all days from start of month to today
|
|
$startTime = strtotime($monthStart);
|
|
$endTime = strtotime($today);
|
|
$currentTime = $startTime;
|
|
|
|
while ($currentTime <= $endTime) {
|
|
$trendDays[] = date('Y-m-d', $currentTime);
|
|
$currentTime = strtotime('+1 day', $currentTime);
|
|
}
|
|
|
|
$opdTrendRaw = $this->db->table('opd_master')
|
|
->select("DATE_FORMAT(apointment_date, '%Y-%m-%d') as yd", false)
|
|
->select('COUNT(opd_id) as total', false)
|
|
->where('apointment_date >=', $trendStart . ' 00:00:00')
|
|
->where('apointment_date <', $tomorrowStart)
|
|
->groupBy('yd')
|
|
->get()
|
|
->getResult();
|
|
|
|
$ipdTrendRaw = $this->db->table('ipd_master')
|
|
->select("DATE_FORMAT(register_date, '%Y-%m-%d') as yd", false)
|
|
->select('COUNT(id) as total', false)
|
|
->where('register_date >=', $trendStart)
|
|
->where('register_date <=', $trendEnd)
|
|
->groupBy('yd')
|
|
->get()
|
|
->getResult();
|
|
|
|
$opdTrendMap = [];
|
|
foreach ($opdTrendRaw as $row) {
|
|
$opdTrendMap[$row->yd] = (int) $row->total;
|
|
}
|
|
$ipdTrendMap = [];
|
|
foreach ($ipdTrendRaw as $row) {
|
|
$ipdTrendMap[$row->yd] = (int) $row->total;
|
|
}
|
|
|
|
$opdTrend = [];
|
|
$ipdTrend = [];
|
|
$trendLabels = [];
|
|
foreach ($trendDays as $dayKey) {
|
|
$trendLabels[] = date('d M', strtotime($dayKey));
|
|
$opdTrend[] = $opdTrendMap[$dayKey] ?? 0;
|
|
$ipdTrend[] = $ipdTrendMap[$dayKey] ?? 0;
|
|
}
|
|
|
|
$data = [
|
|
'user' => $user,
|
|
'opd_today' => $opdToday,
|
|
'opd_last_7_days' => $opdLast7Days,
|
|
'admit_today' => $admitToday,
|
|
'discharge_today' => $dischargeToday,
|
|
'current_ipd' => $currentIpd,
|
|
'current_org_ipd' => $currentOrgIpd,
|
|
'diagnosis_today' => $diagnosisToday,
|
|
'diagnosis_completed_today' => $diagnosisCompletedToday,
|
|
'opd_org_list' => $opdOrgList,
|
|
'opd_doctor_list' => $opdDoctorList,
|
|
'ipd_doctor_list' => $ipdDoctorList,
|
|
'ipd_org_list' => $ipdOrgList,
|
|
'trend_labels' => $trendLabels,
|
|
'trend_opd' => $opdTrend,
|
|
'trend_ipd' => $ipdTrend,
|
|
'month_start' => $monthStart,
|
|
];
|
|
|
|
return view('dashboard/index', $data);
|
|
}
|
|
|
|
public function opdDashboard()
|
|
{
|
|
if (($response = $this->dashboardAccessResponse('dashboard/opd', 'OPD Dashboard')) !== null) {
|
|
return $response;
|
|
}
|
|
|
|
[$fromDate, $toDate, $fromDateTime, $toExclusive] = $this->dashboardDateRange();
|
|
$base = $this->db->table('opd_master')
|
|
->where('apointment_date >=', $fromDateTime)
|
|
->where('apointment_date <', $toExclusive);
|
|
|
|
$totals = $this->db->table('opd_master')
|
|
->select('COUNT(opd_id) total_visits, COUNT(DISTINCT p_id) unique_patients', false)
|
|
->select('SUM(CASE WHEN opd_status = 2 THEN 1 ELSE 0 END) completed_visits', false)
|
|
->select('SUM(CASE WHEN opd_status = 3 THEN 1 ELSE 0 END) cancelled_visits', false)
|
|
->where('apointment_date >=', $fromDateTime)
|
|
->where('apointment_date <', $toExclusive)
|
|
->get()->getRowArray() ?? [];
|
|
|
|
$doctorRows = (clone $base)
|
|
->select("COALESCE(NULLIF(doc_name, ''), 'Unassigned') label", false)
|
|
->select('COUNT(opd_id) total, COUNT(DISTINCT p_id) patients', false)
|
|
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
|
$departmentRows = (clone $base)
|
|
->select("COALESCE(NULLIF(doc_spec, ''), 'Unassigned') label", false)
|
|
->select('COUNT(opd_id) total, COUNT(DISTINCT p_id) patients', false)
|
|
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
|
$organizationRows = (clone $base)
|
|
->select("CASE WHEN o.insurance_id IS NULL OR o.insurance_id <= 1 THEN 'Direct' ELSE COALESCE(NULLIF(ins.short_name, ''), 'Organization') END label", false)
|
|
->select('COUNT(o.opd_id) total', false)
|
|
->join('hc_insurance ins', 'ins.id = o.insurance_id', 'left')
|
|
->from('opd_master o', true)
|
|
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
|
|
|
$referralRows = $this->db->query(
|
|
"SELECT COALESCE(NULLIF(CONCAT_WS(' ', NULLIF(r.title, ''), NULLIF(r.f_name, '')), ''), NULLIF(x.refer_name, ''), 'Not recorded') label,
|
|
COUNT(o.opd_id) total, COUNT(DISTINCT o.p_id) patients
|
|
FROM opd_master o
|
|
LEFT JOIN (
|
|
SELECT opd_code, MAX(NULLIF(refer_by_id, 0)) refer_id,
|
|
MAX(NULLIF(NULLIF(refer_by_other, ''), '0')) refer_name
|
|
FROM invoice_master GROUP BY opd_code
|
|
) x ON x.opd_code = o.opd_code
|
|
LEFT JOIN refer_master r ON r.id = x.refer_id
|
|
WHERE o.apointment_date >= ? AND o.apointment_date < ?
|
|
GROUP BY label ORDER BY total DESC",
|
|
[$fromDateTime, $toExclusive]
|
|
)->getResultArray();
|
|
|
|
return view('dashboard/opd', compact('fromDate', 'toDate', 'totals', 'doctorRows', 'departmentRows', 'organizationRows', 'referralRows'));
|
|
}
|
|
|
|
public function ipdDashboard()
|
|
{
|
|
if (($response = $this->dashboardAccessResponse('dashboard/ipd', 'IPD Dashboard')) !== null) {
|
|
return $response;
|
|
}
|
|
|
|
[$fromDate, $toDate] = $this->dashboardDateRange();
|
|
$totals = $this->db->table('ipd_master')
|
|
->select('COUNT(id) admissions', false)
|
|
->select('SUM(CASE WHEN ipd_status = 0 THEN 1 ELSE 0 END) active_cases', false)
|
|
->where('register_date >=', $fromDate)->where('register_date <=', $toDate)
|
|
->get()->getRowArray() ?? [];
|
|
$totals['discharges'] = $this->db->table('ipd_master')
|
|
->where('ipd_status', 1)->where('discharge_date >=', $fromDate)->where('discharge_date <=', $toDate)
|
|
->countAllResults();
|
|
$totals['current_in_house'] = $this->db->table('ipd_master')->where('ipd_status', 0)->countAllResults();
|
|
|
|
$departmentRows = $this->db->table('ipd_master i')
|
|
->select("COALESCE(NULLIF(d.vName, ''), 'Unassigned') label", false)->select('COUNT(i.id) total', false)
|
|
->join('hc_department d', 'd.iId = i.dept_id', 'left')
|
|
->where('i.register_date >=', $fromDate)->where('i.register_date <=', $toDate)
|
|
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
|
$doctorRows = $this->db->table('ipd_master i')
|
|
->select("COALESCE(NULLIF(d.p_fname, ''), NULLIF(i.r_doc_name, ''), 'Unassigned') label", false)
|
|
->select('COUNT(DISTINCT i.id) total', false)
|
|
->join('ipd_master_doc_list l', 'l.ipd_id = i.id', 'left')
|
|
->join('doctor_master d', 'd.id = l.doc_id', 'left')
|
|
->where('i.register_date >=', $fromDate)->where('i.register_date <=', $toDate)
|
|
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
|
$referralRows = $this->db->table('ipd_master i')
|
|
->select("COALESCE(NULLIF(CONCAT_WS(' ', NULLIF(r.title, ''), NULLIF(r.f_name, '')), ''), 'Not recorded') label", false)
|
|
->select('COUNT(i.id) total', false)
|
|
->join('refer_master r', 'r.id = i.refer_by', 'left')
|
|
->where('i.register_date >=', $fromDate)->where('i.register_date <=', $toDate)
|
|
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
|
$organizationRows = $this->db->table('ipd_master i')
|
|
->select("CASE WHEN i.case_id IS NULL OR i.case_id = 0 THEN 'Direct' ELSE COALESCE(NULLIF(ins.short_name, ''), 'Organization') END label", false)
|
|
->select('COUNT(i.id) total', false)
|
|
->join('organization_case_master oc', 'oc.id = i.case_id', 'left')
|
|
->join('hc_insurance ins', 'ins.id = oc.insurance_id', 'left')
|
|
->where('i.register_date >=', $fromDate)->where('i.register_date <=', $toDate)
|
|
->groupBy('label')->orderBy('total', 'DESC')->get()->getResultArray();
|
|
|
|
return view('dashboard/ipd', compact('fromDate', 'toDate', 'totals', 'doctorRows', 'departmentRows', 'organizationRows', 'referralRows'));
|
|
}
|
|
|
|
public function diagnosisDashboard()
|
|
{
|
|
if (($response = $this->dashboardAccessResponse('dashboard/diagnosis', 'Diagnosis Dashboard')) !== null) {
|
|
return $response;
|
|
}
|
|
|
|
[$fromDate, $toDate] = $this->dashboardDateRange();
|
|
$modalityNames = [1 => 'Ultrasound', 2 => 'MRI', 3 => 'X-Ray', 4 => 'CT Scan', 5 => 'Pathology', 6 => 'Echo', 30 => 'Biopsy'];
|
|
$rows = $this->db->table('invoice_item ii')
|
|
->select('ii.item_type modality_id')->select('COUNT(DISTINCT ii.id) total', false)
|
|
->select('SUM(CASE WHEN lr.id IS NOT NULL THEN 1 ELSE 0 END) samples_received', false)
|
|
->select('SUM(CASE WHEN lr.id IS NULL THEN 1 ELSE 0 END) pending_collection', false)
|
|
->select('SUM(CASE WHEN lr.id IS NOT NULL AND IFNULL(lr.status, 0) < 2 THEN 1 ELSE 0 END) pending_reports', false)
|
|
->select('SUM(CASE WHEN lr.status = 2 THEN 1 ELSE 0 END) completed_reports', false)
|
|
->join('invoice_master im', 'im.id = ii.inv_master_id', 'inner')
|
|
->join('lab_request lr', 'lr.charge_item_id = ii.id AND lr.charge_id = im.id', 'left')
|
|
->whereIn('ii.item_type', array_keys($modalityNames))->where('im.invoice_status', 1)
|
|
->where('im.inv_date >=', $fromDate)->where('im.inv_date <=', $toDate)
|
|
->groupBy('ii.item_type')->get()->getResultArray();
|
|
$modalityRows = [];
|
|
foreach ($modalityNames as $id => $name) {
|
|
$match = array_values(array_filter($rows, static fn (array $row): bool => (int) $row['modality_id'] === $id))[0] ?? [];
|
|
$modalityRows[] = array_merge(['modality_id' => $id, 'label' => $name, 'total' => 0, 'samples_received' => 0, 'pending_collection' => 0, 'pending_reports' => 0, 'completed_reports' => 0], $match);
|
|
}
|
|
$totals = ['total' => 0, 'samples_received' => 0, 'pending_collection' => 0, 'pending_reports' => 0, 'completed_reports' => 0];
|
|
foreach ($modalityRows as $row) {
|
|
foreach (array_keys($totals) as $key) {
|
|
$totals[$key] += (int) $row[$key];
|
|
}
|
|
}
|
|
|
|
return view('dashboard/diagnosis', compact('fromDate', 'toDate', 'totals', 'modalityRows'));
|
|
}
|
|
|
|
private function dashboardAccessResponse(string $route, string $title)
|
|
{
|
|
if (($authRedirect = $this->ensureAuthenticated()) !== null) {
|
|
return $authRedirect;
|
|
}
|
|
if (! $this->request->isAJAX()) {
|
|
return view('welcome_message', ['initial_route' => base_url($route), 'initial_title' => $title]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/** @return array{0:string,1:string,2:string,3:string} */
|
|
private function dashboardDateRange(): array
|
|
{
|
|
$today = date('Y-m-d');
|
|
$fromDate = (string) ($this->request->getGet('from_date') ?: $today);
|
|
$toDate = (string) ($this->request->getGet('to_date') ?: $fromDate);
|
|
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $fromDate) || strtotime($fromDate) === false) {
|
|
$fromDate = $today;
|
|
}
|
|
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $toDate) || strtotime($toDate) === false) {
|
|
$toDate = $fromDate;
|
|
}
|
|
if ($fromDate > $toDate) {
|
|
[$fromDate, $toDate] = [$toDate, $fromDate];
|
|
}
|
|
return [$fromDate, $toDate, $fromDate . ' 00:00:00', date('Y-m-d', strtotime($toDate . ' +1 day')) . ' 00:00:00'];
|
|
}
|
|
|
|
public function myProfile()
|
|
{
|
|
$authRedirect = $this->ensureAuthenticated();
|
|
if ($authRedirect !== null) {
|
|
return $authRedirect;
|
|
}
|
|
|
|
if (! $this->request->isAJAX()) {
|
|
return view('welcome_message', [
|
|
'initial_route' => base_url('my-profile'),
|
|
'initial_title' => 'My Profile',
|
|
]);
|
|
}
|
|
|
|
$user = service('auth')->user();
|
|
$userId = (int) ($user->id ?? 0);
|
|
$meta = $this->getCurrentUserProfileMeta($userId);
|
|
|
|
// Load user settings (use hospital defaults if not set)
|
|
$userSettingsModel = model('UserSettings');
|
|
$sidebarAutoHideSecondsUser = $userSettingsModel->getUserSetting(
|
|
$userId,
|
|
'SIDEBAR_AUTO_HIDE_SECONDS',
|
|
hospital_setting_value('SIDEBAR_AUTO_HIDE_SECONDS', '7')
|
|
);
|
|
|
|
return view('account/my_profile', [
|
|
'user' => $user,
|
|
'phone_no' => $meta['phone_no'] ?? '',
|
|
'person_name' => $meta['full_name'] ?? '',
|
|
'profile_photo_url' => $this->buildUserProfilePhotoUrl((string) ($meta['profile_photo'] ?? '')),
|
|
'sidebar_auto_hide_seconds' => $sidebarAutoHideSecondsUser,
|
|
]);
|
|
}
|
|
|
|
public function speechTest()
|
|
{
|
|
$authRedirect = $this->ensureAuthenticated();
|
|
if ($authRedirect !== null) {
|
|
return $authRedirect;
|
|
}
|
|
|
|
return view('tools/speech_test');
|
|
}
|
|
|
|
/**
|
|
* Server-side proxy for the STT service.
|
|
* Forwards audio from HTTPS clients to the HTTP STT server,
|
|
* avoiding browser mixed-content blocks.
|
|
*/
|
|
public function sttProxy()
|
|
{
|
|
$authRedirect = $this->ensureAuthenticated();
|
|
if ($authRedirect !== null) {
|
|
return $this->response->setStatusCode(401)->setJSON(['error' => 'Unauthorized']);
|
|
}
|
|
|
|
$file = $this->request->getFile('audio');
|
|
if ($file === null) {
|
|
// Fallback key for compatibility with alternate STT clients.
|
|
$file = $this->request->getFile('file');
|
|
}
|
|
|
|
if ($file === null) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'error' => 'No audio file received',
|
|
'expected_field' => 'audio',
|
|
]);
|
|
}
|
|
|
|
if (! $file->isValid()) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'error' => 'Invalid uploaded audio file',
|
|
'upload_error_code' => $file->getError(),
|
|
'upload_error' => $file->getErrorString(),
|
|
'upload_max_filesize' => (string) ini_get('upload_max_filesize'),
|
|
'post_max_size' => (string) ini_get('post_max_size'),
|
|
]);
|
|
}
|
|
|
|
$fileSize = (int) $file->getSize();
|
|
if ($fileSize <= 0) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'error' => 'Empty audio received',
|
|
]);
|
|
}
|
|
|
|
$lang = (string) ($this->request->getPost('lang') ?? 'en-IN');
|
|
$context = (string) ($this->request->getPost('medical_context') ?? '');
|
|
|
|
$sttUrl = 'http://139.59.13.39:8000/stt/transcribe';
|
|
|
|
$tmpPath = $file->getTempName();
|
|
$mime = $file->getMimeType() ?: 'audio/webm';
|
|
$name = $file->getName() ?: 'audio.webm';
|
|
|
|
$ch = curl_init($sttUrl);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_POST => true,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 30,
|
|
CURLOPT_POSTFIELDS => [
|
|
'audio' => new \CURLFile($tmpPath, $mime, $name),
|
|
'lang' => $lang,
|
|
'medical_context' => $context,
|
|
],
|
|
]);
|
|
|
|
$body = curl_exec($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlErr = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($body === false || $curlErr !== '') {
|
|
return $this->response->setStatusCode(502)
|
|
->setJSON(['error' => 'STT server unreachable', 'detail' => $curlErr]);
|
|
}
|
|
|
|
$decoded = json_decode((string) $body, true);
|
|
if ($decoded === null) {
|
|
return $this->response->setStatusCode(502)
|
|
->setJSON(['error' => 'Invalid response from STT server']);
|
|
}
|
|
|
|
return $this->response->setStatusCode($httpCode > 0 ? $httpCode : 200)
|
|
->setContentType('application/json')
|
|
->setBody($body);
|
|
}
|
|
|
|
/**
|
|
* Proxy health check — pings the STT server and returns its status.
|
|
* Lets the browser check availability over HTTPS without mixed-content issues.
|
|
*/
|
|
public function sttProxyHealth()
|
|
{
|
|
$authRedirect = $this->ensureAuthenticated();
|
|
if ($authRedirect !== null) {
|
|
return $this->response->setStatusCode(401)->setJSON(['ok' => false]);
|
|
}
|
|
|
|
$ch = curl_init('http://139.59.13.39:8000/health');
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 4,
|
|
CURLOPT_NOBODY => false,
|
|
]);
|
|
$body = curl_exec($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
|
|
$ok = ($body !== false && $httpCode >= 200 && $httpCode < 300);
|
|
return $this->response->setStatusCode(200)->setJSON(['ok' => $ok]);
|
|
}
|
|
|
|
public function myProfileSave()
|
|
{
|
|
$authRedirect = $this->ensureAuthenticated();
|
|
if ($authRedirect !== null) {
|
|
return $authRedirect;
|
|
}
|
|
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid request',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$authUser = service('auth')->user();
|
|
$userId = (int) ($authUser->id ?? 0);
|
|
if ($userId <= 0) {
|
|
return $this->response->setStatusCode(401)->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Unauthorized',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$email = trim((string) $this->request->getPost('email'));
|
|
$personName = trim((string) $this->request->getPost('person_name'));
|
|
$phoneNo = trim((string) $this->request->getPost('phone_no'));
|
|
$password = (string) $this->request->getPost('password');
|
|
$passwordConfirm = (string) $this->request->getPost('password_confirm');
|
|
$metaBeforeUpdate = $this->getCurrentUserProfileMeta($userId);
|
|
|
|
$profilePhotoFile = $this->request->getFile('profile_photo');
|
|
if ($profilePhotoFile !== null && $profilePhotoFile->getError() !== UPLOAD_ERR_NO_FILE && ! $profilePhotoFile->isValid()) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid profile photo upload.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$photoProvided = $profilePhotoFile !== null
|
|
&& $profilePhotoFile->isValid()
|
|
&& $profilePhotoFile->getError() !== UPLOAD_ERR_NO_FILE;
|
|
$profilePhotoFileName = '';
|
|
|
|
if ($photoProvided) {
|
|
$photoSize = (int) $profilePhotoFile->getSize();
|
|
$photoMime = strtolower((string) $profilePhotoFile->getMimeType());
|
|
$allowedMime = [
|
|
'image/jpeg',
|
|
'image/jpg',
|
|
'image/png',
|
|
'image/webp',
|
|
];
|
|
|
|
if ($photoSize <= 0 || $photoSize > (2 * 1024 * 1024)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Profile photo must be less than 2MB.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if (! in_array($photoMime, $allowedMime, true)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Profile photo must be JPG, PNG, or WEBP.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$extension = strtolower((string) $profilePhotoFile->getExtension());
|
|
if ($extension === '') {
|
|
$extension = $photoMime === 'image/png' ? 'png' : ($photoMime === 'image/webp' ? 'webp' : 'jpg');
|
|
}
|
|
$profilePhotoFileName = 'user_' . $userId . '_' . date('YmdHis') . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
|
|
}
|
|
|
|
if ($personName !== '' && mb_strlen($personName) > 120) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Person name must be maximum 120 characters.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if ($email === '' || ! filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Valid email is required.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if ($phoneNo !== '' && preg_match('/^[0-9+\-()\s]{7,20}$/', $phoneNo) !== 1) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Phone format is invalid.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if ($password !== '' || $passwordConfirm !== '') {
|
|
if (strlen($password) < 8) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Password must be at least 8 characters.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if (! hash_equals($password, $passwordConfirm)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Password and confirm password do not match.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
$tables = config('Auth')->tables;
|
|
$identitiesTable = (string) ($tables['identities'] ?? 'auth_identities');
|
|
|
|
$duplicateEmail = $this->db->table($identitiesTable)
|
|
->select('id')
|
|
->where('type', 'email_password')
|
|
->where('secret', $email)
|
|
->where('user_id !=', $userId)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
|
|
if (! empty($duplicateEmail)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Email already used by another user.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$userModel = model(UserModel::class);
|
|
$user = $userModel->find($userId);
|
|
if ($user === null) {
|
|
return $this->response->setStatusCode(404)->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'User not found',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$user->email = $email;
|
|
if ($password !== '') {
|
|
$user->password = $password;
|
|
}
|
|
|
|
if (! $userModel->save($user)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Unable to update profile.',
|
|
'errors' => $userModel->errors(),
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if ($photoProvided) {
|
|
$uploadDir = FCPATH . self::USER_PROFILE_PHOTO_DIR;
|
|
if (! is_dir($uploadDir) && ! @mkdir($uploadDir, 0755, true) && ! is_dir($uploadDir)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Unable to create profile photo directory.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
try {
|
|
$profilePhotoFile->move($uploadDir, $profilePhotoFileName, true);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Unable to upload profile photo.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$oldPhoto = trim((string) ($metaBeforeUpdate['profile_photo'] ?? ''));
|
|
if ($oldPhoto !== '' && $oldPhoto !== $profilePhotoFileName) {
|
|
$oldPhotoPath = $uploadDir . DIRECTORY_SEPARATOR . basename($oldPhoto);
|
|
if (is_file($oldPhotoPath)) {
|
|
@unlink($oldPhotoPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->setCurrentUserProfileMeta(
|
|
$userId,
|
|
$phoneNo,
|
|
$personName,
|
|
$photoProvided ? $profilePhotoFileName : null
|
|
);
|
|
|
|
// Save user settings
|
|
$sidebarAutoHideSeconds = (int) trim((string) $this->request->getPost('sidebar_auto_hide_seconds'));
|
|
if ($sidebarAutoHideSeconds < 0) {
|
|
$sidebarAutoHideSeconds = 0;
|
|
}
|
|
if ($sidebarAutoHideSeconds > 60) {
|
|
$sidebarAutoHideSeconds = 60;
|
|
}
|
|
|
|
$userSettingsModel = model('UserSettings');
|
|
if ($sidebarAutoHideSeconds > 0) {
|
|
$userSettingsModel->setUserSetting($userId, 'SIDEBAR_AUTO_HIDE_SECONDS', (string) $sidebarAutoHideSeconds);
|
|
} else {
|
|
// Delete if zero to use hospital default
|
|
$userSettingsModel->deleteUserSetting($userId, 'SIDEBAR_AUTO_HIDE_SECONDS');
|
|
}
|
|
|
|
$metaAfterUpdate = $this->getCurrentUserProfileMeta($userId);
|
|
|
|
$displayName = trim((string) ($user->username ?? ''));
|
|
if ($personName !== '') {
|
|
$displayName = $personName;
|
|
} elseif ($displayName === '') {
|
|
$displayName = trim((string) ($email !== '' ? $email : 'User'));
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => 'Profile updated successfully.',
|
|
'display_name' => $displayName,
|
|
'person_name' => $personName,
|
|
'login_id' => trim((string) ($user->username ?? '')),
|
|
'user_id' => $userId,
|
|
'email' => $email,
|
|
'phone_no' => $phoneNo,
|
|
'profile_photo_url' => $this->buildUserProfilePhotoUrl((string) ($metaAfterUpdate['profile_photo'] ?? '')),
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
public function myProfileSaveSettings()
|
|
{
|
|
if (! $this->request->is('post')) {
|
|
return $this->response->setStatusCode(405)->setJSON([
|
|
'success' => 0,
|
|
'message' => 'Method not allowed.',
|
|
]);
|
|
}
|
|
|
|
$authUser = auth()->user();
|
|
if (! $authUser) {
|
|
return $this->response->setStatusCode(401)->setJSON([
|
|
'success' => 0,
|
|
'message' => 'Not authenticated.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$userId = (int) $authUser->id;
|
|
|
|
// Process sidebar auto-hide setting
|
|
$sidebarAutoHideSecondsInput = trim((string) $this->request->getPost('sidebar_auto_hide_seconds'));
|
|
$userSettingsModel = model('UserSettings');
|
|
|
|
if ($sidebarAutoHideSecondsInput === '') {
|
|
// Empty means use hospital default
|
|
$userSettingsModel->deleteUserSetting($userId, 'SIDEBAR_AUTO_HIDE_SECONDS');
|
|
$message = 'Settings saved. Using hospital default.';
|
|
} else {
|
|
$sidebarAutoHideSeconds = (int) $sidebarAutoHideSecondsInput;
|
|
if ($sidebarAutoHideSeconds < 0) {
|
|
$sidebarAutoHideSeconds = 0;
|
|
}
|
|
if ($sidebarAutoHideSeconds > 60) {
|
|
$sidebarAutoHideSeconds = 60;
|
|
}
|
|
|
|
if ($sidebarAutoHideSeconds > 0) {
|
|
$userSettingsModel->setUserSetting($userId, 'SIDEBAR_AUTO_HIDE_SECONDS', (string) $sidebarAutoHideSeconds);
|
|
$message = "Sidebar auto-hide set to {$sidebarAutoHideSeconds} seconds.";
|
|
} else {
|
|
// 0 means disable auto-hide
|
|
$userSettingsModel->setUserSetting($userId, 'SIDEBAR_AUTO_HIDE_SECONDS', '0');
|
|
$message = 'Sidebar auto-hide disabled.';
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'success' => 1,
|
|
'message' => $message,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array{phone_no:string, full_name:string, profile_photo:string}
|
|
*/
|
|
private function getCurrentUserProfileMeta(int $userId): array
|
|
{
|
|
if ($userId <= 0) {
|
|
return ['phone_no' => '', 'full_name' => '', 'profile_photo' => ''];
|
|
}
|
|
|
|
$tables = config('Auth')->tables;
|
|
$identitiesTable = (string) ($tables['identities'] ?? 'auth_identities');
|
|
if (! $this->db->tableExists($identitiesTable)) {
|
|
return ['phone_no' => '', 'full_name' => '', 'profile_photo' => ''];
|
|
}
|
|
|
|
$row = $this->db->table($identitiesTable)
|
|
->select('extra')
|
|
->where('user_id', $userId)
|
|
->where('type', 'email_password')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
$extraRaw = trim((string) ($row['extra'] ?? ''));
|
|
if ($extraRaw === '') {
|
|
return ['phone_no' => '', 'full_name' => '', 'profile_photo' => ''];
|
|
}
|
|
|
|
$decoded = json_decode($extraRaw, true);
|
|
if (! is_array($decoded)) {
|
|
return ['phone_no' => '', 'full_name' => '', 'profile_photo' => ''];
|
|
}
|
|
|
|
return [
|
|
'phone_no' => trim((string) ($decoded['phone_no'] ?? '')),
|
|
'full_name' => trim((string) ($decoded['full_name'] ?? '')),
|
|
'profile_photo' => trim((string) ($decoded['profile_photo'] ?? '')),
|
|
];
|
|
}
|
|
|
|
private function setCurrentUserProfileMeta(int $userId, string $phoneNo, string $fullName, ?string $profilePhoto = null): void
|
|
{
|
|
if ($userId <= 0) {
|
|
return;
|
|
}
|
|
|
|
$tables = config('Auth')->tables;
|
|
$identitiesTable = (string) ($tables['identities'] ?? 'auth_identities');
|
|
if (! $this->db->tableExists($identitiesTable)) {
|
|
return;
|
|
}
|
|
|
|
$row = $this->db->table($identitiesTable)
|
|
->select('id,extra')
|
|
->where('user_id', $userId)
|
|
->where('type', 'email_password')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (empty($row) || empty($row['id'])) {
|
|
return;
|
|
}
|
|
|
|
$payload = [];
|
|
$raw = trim((string) ($row['extra'] ?? ''));
|
|
if ($raw !== '') {
|
|
$decoded = json_decode($raw, true);
|
|
if (is_array($decoded)) {
|
|
$payload = $decoded;
|
|
}
|
|
}
|
|
|
|
if ($phoneNo === '') {
|
|
unset($payload['phone_no']);
|
|
} else {
|
|
$payload['phone_no'] = $phoneNo;
|
|
}
|
|
|
|
if ($fullName === '') {
|
|
unset($payload['full_name']);
|
|
} else {
|
|
$payload['full_name'] = $fullName;
|
|
}
|
|
|
|
if ($profilePhoto !== null) {
|
|
$profilePhoto = trim($profilePhoto);
|
|
if ($profilePhoto === '') {
|
|
unset($payload['profile_photo']);
|
|
} else {
|
|
$payload['profile_photo'] = basename($profilePhoto);
|
|
}
|
|
}
|
|
|
|
$this->db->table($identitiesTable)
|
|
->where('id', (int) $row['id'])
|
|
->update([
|
|
'extra' => empty($payload) ? null : json_encode($payload, JSON_UNESCAPED_UNICODE),
|
|
]);
|
|
}
|
|
|
|
private function buildUserProfilePhotoUrl(string $photoFileName): string
|
|
{
|
|
$photoFileName = trim($photoFileName);
|
|
if ($photoFileName === '') {
|
|
return base_url('assets/img/profile-img.jpg');
|
|
}
|
|
|
|
$absolutePath = FCPATH . self::USER_PROFILE_PHOTO_DIR . DIRECTORY_SEPARATOR . basename($photoFileName);
|
|
if (! is_file($absolutePath)) {
|
|
return base_url('assets/img/profile-img.jpg');
|
|
}
|
|
|
|
return base_url(self::USER_PROFILE_PHOTO_DIR . '/' . basename($photoFileName));
|
|
}
|
|
}
|