From 1bac1f6e0428d3be8d8a6e86b1ff7c6edcc41088 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Thu, 18 Jun 2026 17:38:20 +0530 Subject: [PATCH] feat: update storestock flow and ABDM bridge handling --- app/Controllers/Opd_prescription.php | 391 ++++++++++- app/Controllers/Patient.php | 290 ++++++++ .../Setting/AbdmGatewaySettings.php | 16 +- app/Controllers/Storestock.php | 663 +++++++++++++++++- app/Libraries/Abdm/EAtriaBridgeConnector.php | 43 +- app/Views/billing/opd_invoice_V.php | 10 +- app/Views/billing/opd_prescription_basic.php | 127 ++-- app/Views/partials/abha_otp_modal.php | 60 +- app/Views/storestock/dashboard.php | 36 +- app/Views/storestock/dashboard_sub.php | 131 ++++ app/Views/storestock/day_report.php | 88 ++- app/Views/storestock/indent_list.php | 108 ++- app/Views/storestock/purchase.php | 97 +++ .../purchase_invoice_master_edit.php | 136 ++++ app/Views/storestock/purchase_new_invoice.php | 60 ++ app/Views/storestock/purchase_print.php | 51 ++ .../purchase_return_invoice_edit.php | 24 + .../purchase_return_invoice_item.php | 28 + app/Views/storestock/purchase_return_new.php | 50 ++ app/Views/storestock/purchase_supp_list.php | 51 ++ 20 files changed, 2264 insertions(+), 196 deletions(-) create mode 100644 app/Views/storestock/dashboard_sub.php create mode 100644 app/Views/storestock/purchase.php create mode 100644 app/Views/storestock/purchase_invoice_master_edit.php create mode 100644 app/Views/storestock/purchase_new_invoice.php create mode 100644 app/Views/storestock/purchase_print.php create mode 100644 app/Views/storestock/purchase_return_invoice_edit.php create mode 100644 app/Views/storestock/purchase_return_invoice_item.php create mode 100644 app/Views/storestock/purchase_return_new.php create mode 100644 app/Views/storestock/purchase_supp_list.php diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 53a763b..e23b8f1 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -7133,22 +7133,9 @@ class Opd_prescription extends BaseController $code = trim((string) $this->request->getPost('investigation_code')); $name = trim((string) $this->request->getPost('investigation_name')); - // Profile chips may send only investigation name; resolve code from master to avoid unique-key collisions. - if ($code === '' && $name !== '' && $this->db->tableExists('investigation')) { - $invFields = $this->db->getFieldNames('investigation'); - $invCodeField = $this->resolveFirstField($invFields, ['Code', 'code']); - $invNameField = $this->resolveFirstField($invFields, ['Name', 'name']); - if ($invCodeField !== null && $invNameField !== null) { - $resolved = $this->db->table('investigation') - ->select($invCodeField . ' as code') - ->where($invNameField, $name) - ->limit(1) - ->get() - ->getRowArray(); - if (! empty($resolved['code'])) { - $code = trim((string) $resolved['code']); - } - } + // Resolve profile shorthand names (CBC/LFT/KFT etc.) to canonical codes. + if (($code === '' || $code === '0') && $name !== '') { + $code = $this->resolveInvestigationCodeByText($name, $this->buildInvestigationLookupMaps()); } if ($opdId <= 0 || ($code === '' && $name === '')) { @@ -7229,6 +7216,303 @@ class Opd_prescription extends BaseController ]); } + public function investigation_add_bulk() + { + $startedAt = microtime(true); + $respond = function (array $payload, int $statusCode = 200) use ($startedAt) { + $payload['elapsed_ms'] = (int) round((microtime(true) - $startedAt) * 1000); + return $this->response->setStatusCode($statusCode)->setJSON($payload); + }; + + if (! $this->request->isAJAX()) { + return $respond(['update' => 0, 'error_text' => 'Invalid request'], 400); + } + + $opdId = (int) $this->request->getPost('opd_id'); + $sessionId = (int) $this->request->getPost('opd_session_id'); + + $rawTests = $this->request->getPost('tests'); + $tests = []; + if (is_string($rawTests) && $rawTests !== '') { + $decoded = json_decode($rawTests, true); + if (is_array($decoded)) { + $tests = $decoded; + } + } elseif (is_array($rawTests)) { + $tests = $rawTests; + } + + if ($opdId <= 0 || empty($tests)) { + return $respond(['update' => 0, 'error_text' => 'Invalid bulk payload']); + } + + $table = $this->findExistingTable(['opd_prescription_investigation']); + if ($table === null) { + return $respond(['update' => 0, 'error_text' => 'Investigation table not found']); + } + + $sessionId = $this->ensurePrescriptionSession($opdId, $sessionId); + if ($sessionId <= 0) { + return $respond(['update' => 0, 'error_text' => 'Unable to create prescription session']); + } + + $fields = $this->db->getFieldNames($table); + $hasOpdPreId = in_array('opd_pre_id', $fields, true); + $hasOpdId = in_array('opd_id', $fields, true); + $hasCode = in_array('investigation_code', $fields, true); + $hasName = in_array('investigation_name', $fields, true); + + $existingRowsBuilder = $this->db->table($table); + if ($hasOpdPreId) { + $existingRowsBuilder->where('opd_pre_id', $sessionId); + } elseif ($hasOpdId) { + $existingRowsBuilder->where('opd_id', $opdId); + } else { + return $respond(['update' => 0, 'error_text' => 'Invalid investigation table structure']); + } + + $existingRows = $existingRowsBuilder->get()->getResultArray(); + $existingByCode = []; + $existingByName = []; + foreach ($existingRows as $row) { + $exCode = trim((string) ($row['investigation_code'] ?? '')); + $exName = trim((string) ($row['investigation_name'] ?? '')); + if ($exCode !== '' && $exCode !== '0') { + $existingByCode[strtolower($exCode)] = true; + } + if ($exName !== '') { + $existingByName[strtolower($exName)] = true; + } + } + + $invLookup = $this->buildInvestigationLookupMaps(); + + $seenPayload = []; + $insertRows = []; + $added = 0; + $skipped = 0; + + foreach ($tests as $test) { + $name = trim((string) (($test['name'] ?? $test['investigation_name'] ?? ''))); + $code = trim((string) (($test['code'] ?? $test['investigation_code'] ?? ''))); + + if ($name === '' && $code === '') { + $skipped++; + continue; + } + + if (($code === '' || $code === '0') && $name !== '') { + $code = $this->resolveInvestigationCodeByText($name, $invLookup); + } + + $payloadKey = strtolower($code . '|' . $name); + if (isset($seenPayload[$payloadKey])) { + $skipped++; + continue; + } + $seenPayload[$payloadKey] = true; + + $duplicateByCode = ($code !== '' && isset($existingByCode[strtolower($code)])); + $duplicateByName = ($name !== '' && isset($existingByName[strtolower($name)])); + if ($duplicateByCode || $duplicateByName) { + $skipped++; + continue; + } + + $row = []; + if ($hasOpdId) { + $row['opd_id'] = $opdId; + } + if ($hasOpdPreId) { + $row['opd_pre_id'] = $sessionId; + } + if ($hasCode) { + $row['investigation_code'] = ($code === '' || $code === '0') ? '' : $code; + } + if ($hasName) { + $row['investigation_name'] = $name; + } + $insertRows[] = $row; + + if ($code !== '' && $code !== '0') { + $existingByCode[strtolower($code)] = true; + } + if ($name !== '') { + $existingByName[strtolower($name)] = true; + } + } + + foreach ($insertRows as $row) { + try { + $this->db->table($table)->insert($row); + $added++; + $insertId = (int) $this->db->insertID(); + $this->auditClinicalUpdate('opd_prescription_investigation', 'added', $insertId, null, $row); + } catch (\Throwable $e) { + $msg = strtolower($e->getMessage()); + if (strpos($msg, 'duplicate entry') !== false) { + $skipped++; + continue; + } + throw $e; + } + } + + $result = [ + 'update' => 1, + 'opd_session_id' => $sessionId, + 'added_count' => $added, + 'skipped_count' => $skipped, + 'total_count' => count($tests), + 'error_text' => $added > 0 ? 'Investigations added' : 'No new investigations added', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]; + + log_message( + 'info', + 'investigation_add_bulk opd_id={opd_id} session_id={session_id} total={total} added={added} skipped={skipped} elapsed_ms={elapsed_ms}', + [ + 'opd_id' => $opdId, + 'session_id' => $sessionId, + 'total' => count($tests), + 'added' => $added, + 'skipped' => $skipped, + 'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000), + ] + ); + + return $respond($result); + } + + private function buildInvestigationLookupMaps(): array + { + $out = [ + 'by_name' => [], + 'by_short' => [], + 'by_alias' => [], + ]; + + if (! $this->db->tableExists('investigation')) { + return $out; + } + + $invFields = $this->db->getFieldNames('investigation'); + $invCodeField = $this->resolveFirstField($invFields, ['Code', 'code']); + $invNameField = $this->resolveFirstField($invFields, ['Name', 'name']); + $invShortField = $this->resolveFirstField($invFields, ['short_name', 'shortName', 'short']); + + if ($invCodeField === null || $invNameField === null) { + return $out; + } + + $select = $invCodeField . ' as code, ' . $invNameField . ' as name'; + if ($invShortField !== null) { + $select .= ', ' . $invShortField . ' as short_name'; + } + + $rows = $this->db->table('investigation')->select($select)->get()->getResultArray(); + foreach ($rows as $row) { + $code = trim((string) ($row['code'] ?? '')); + if ($code === '' || $code === '0') { + continue; + } + + $name = trim((string) ($row['name'] ?? '')); + $short = trim((string) ($row['short_name'] ?? '')); + + $nameKey = $this->normalizeInvestigationLookupKey($name); + if ($nameKey !== '' && ! isset($out['by_name'][$nameKey])) { + $out['by_name'][$nameKey] = $code; + } + + $shortKey = $this->normalizeInvestigationLookupKey($short); + if ($shortKey !== '' && ! isset($out['by_short'][$shortKey])) { + $out['by_short'][$shortKey] = $code; + } + + if (preg_match('/\(([A-Za-z0-9\-\s\.]{2,20})\)/', $name, $m) === 1) { + $aliasKey = $this->normalizeInvestigationLookupKey((string) ($m[1] ?? '')); + if ($aliasKey !== '' && ! isset($out['by_alias'][$aliasKey])) { + $out['by_alias'][$aliasKey] = $code; + } + } + + $acronym = $this->buildInvestigationAcronym($name); + if ($acronym !== '' && ! isset($out['by_alias'][$acronym])) { + $out['by_alias'][$acronym] = $code; + } + } + + return $out; + } + + private function resolveInvestigationCodeByText(string $text, array $lookup): string + { + $key = $this->normalizeInvestigationLookupKey($text); + if ($key === '') { + return ''; + } + + if (isset($lookup['by_name'][$key])) { + return (string) $lookup['by_name'][$key]; + } + if (isset($lookup['by_short'][$key])) { + return (string) $lookup['by_short'][$key]; + } + if (isset($lookup['by_alias'][$key])) { + return (string) $lookup['by_alias'][$key]; + } + + // Common profile aliases seen in legacy shortcuts. + $aliasFallback = [ + 'kft' => 'rft', + ]; + $fallback = $aliasFallback[$key] ?? ''; + if ($fallback !== '' && isset($lookup['by_alias'][$fallback])) { + return (string) $lookup['by_alias'][$fallback]; + } + if ($fallback !== '' && isset($lookup['by_short'][$fallback])) { + return (string) $lookup['by_short'][$fallback]; + } + + return ''; + } + + private function normalizeInvestigationLookupKey(string $value): string + { + $value = trim(strtolower($value)); + if ($value === '') { + return ''; + } + return (string) (preg_replace('/[^a-z0-9]+/', '', $value) ?? ''); + } + + private function buildInvestigationAcronym(string $name): string + { + $plain = preg_replace('/\([^)]*\)/', ' ', $name) ?? $name; + $plain = trim((string) preg_replace('/\s+/', ' ', $plain)); + if ($plain === '') { + return ''; + } + + $stopWords = [ + 'and', 'of', 'for', 'the', 'test', 'panel', 'routine', 'view', 'complete', 'count', + ]; + + $parts = preg_split('/\s+/', strtolower($plain)) ?: []; + $acronym = ''; + foreach ($parts as $part) { + $part = trim((string) preg_replace('/[^a-z0-9]/', '', $part)); + if ($part === '' || in_array($part, $stopWords, true)) { + continue; + } + $acronym .= substr($part, 0, 1); + } + + return strlen($acronym) >= 2 ? $acronym : ''; + } + public function investigation_remove($id) { $table = $this->findExistingTable(['opd_prescription_investigation']); @@ -7247,6 +7531,81 @@ class Opd_prescription extends BaseController return $this->response->setJSON(['update' => 1, 'error_text' => 'Investigation removed']); } + public function investigation_remove_bulk() + { + $startedAt = microtime(true); + $respond = function (array $payload, int $statusCode = 200) use ($startedAt) { + $payload['elapsed_ms'] = (int) round((microtime(true) - $startedAt) * 1000); + return $this->response->setStatusCode($statusCode)->setJSON($payload); + }; + + if (! $this->request->isAJAX()) { + return $respond(['update' => 0, 'error_text' => 'Invalid request'], 400); + } + + $rawIds = $this->request->getPost('ids'); + $ids = []; + + if (is_array($rawIds)) { + $ids = $rawIds; + } elseif (is_string($rawIds) && trim($rawIds) !== '') { + $decoded = json_decode($rawIds, true); + if (is_array($decoded)) { + $ids = $decoded; + } else { + $ids = explode(',', $rawIds); + } + } + + $ids = array_values(array_unique(array_filter(array_map(static function ($id) { + return (int) $id; + }, $ids), static function ($id) { + return $id > 0; + }))); + + if (empty($ids)) { + return $respond(['update' => 0, 'error_text' => 'No investigations selected']); + } + + $table = $this->findExistingTable(['opd_prescription_investigation']); + if ($table === null) { + return $respond(['update' => 0, 'error_text' => 'Investigation table not found']); + } + + $fields = $this->db->getFieldNames($table); + if (! in_array('id', $fields, true)) { + return $respond(['update' => 0, 'error_text' => 'Investigation id field missing']); + } + + $rows = $this->db->table($table)->whereIn('id', $ids)->get()->getResultArray(); + $this->db->table($table)->whereIn('id', $ids)->delete(); + + foreach ($rows as $row) { + $this->auditClinicalUpdate('opd_prescription_investigation', 'removed', (int) ($row['id'] ?? 0), $row, null); + } + + $result = [ + 'update' => 1, + 'removed_count' => count($rows), + 'requested_count' => count($ids), + 'error_text' => 'Investigations removed', + 'csrfName' => csrf_token(), + 'csrfHash' => csrf_hash(), + ]; + + log_message( + 'info', + 'investigation_remove_bulk requested={requested} removed={removed} elapsed_ms={elapsed_ms}', + [ + 'requested' => count($ids), + 'removed' => count($rows), + 'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000), + ] + ); + + return $respond($result); + } + public function medicine_search() { $q = trim((string) $this->request->getGet('q')); diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index 80ce487..ac931da 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -341,6 +341,7 @@ class Patient extends BaseController $txnId = trim((string) ($body['txnId'] ?? $body['txn_id'] ?? $this->request->getPost('txnId') ?? $this->request->getPost('txn_id') ?? '')); $otp = trim((string) ($body['otp'] ?? $this->request->getPost('otp') ?? '')); $mobile = trim((string) ($body['mobile'] ?? $this->request->getPost('mobile') ?? '')); + $requestedPid = (int) ($body['p_id'] ?? $this->request->getPost('p_id') ?? 0); if ($txnId === '' || $otp === '') { return $this->response->setJSON(['ok' => 0, 'error_text' => 'txnId and otp are required']); @@ -352,6 +353,10 @@ class Patient extends BaseController return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]); } + if (! empty($result['ok']) && (int) $result['ok'] === 1) { + $result = $this->enrichAbhaVerifyResult($result, $requestedPid); + } + return $this->response->setJSON($result); } @@ -388,6 +393,7 @@ class Patient extends BaseController $body = $isJson ? ($this->request->getJSON(true) ?? []) : []; $txnId = trim((string) ($body['txnId'] ?? $body['txn_id'] ?? $this->request->getPost('txnId') ?? $this->request->getPost('txn_id') ?? '')); $otp = trim((string) ($body['otp'] ?? $this->request->getPost('otp') ?? '')); + $requestedPid = (int) ($body['p_id'] ?? $this->request->getPost('p_id') ?? 0); if ($txnId === '' || $otp === '') { return $this->response->setJSON(['ok' => 0, 'error_text' => 'txnId and otp are required']); @@ -399,6 +405,10 @@ class Patient extends BaseController return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]); } + if (! empty($result['ok']) && (int) $result['ok'] === 1) { + $result = $this->enrichAbhaVerifyResult($result, $requestedPid); + } + return $this->response->setJSON($result); } @@ -1332,6 +1342,286 @@ class Patient extends BaseController return (int) $this->db->insertID(); } + /** + * @param array $result + * @return array + */ + private function enrichAbhaVerifyResult(array $result, int $requestedPid): array + { + $payload = []; + if (isset($result['data']) && is_array($result['data'])) { + $payload = $result['data']; + } elseif (is_array($result)) { + $payload = $result; + } + + $profile = $this->pickGatewayAbhaProfile($payload); + $abhaNumberRaw = trim((string) ($profile['ABHANumber'] ?? $payload['ABHANumber'] ?? '')); + $abhaDigits = preg_replace('/\D/', '', $abhaNumberRaw); + $abhaAddress = trim((string) ($profile['preferredAbhaAddress'] ?? $payload['abha_address'] ?? '')); + $gatewayName = trim((string) ($profile['name'] ?? $payload['name'] ?? '')); + $gatewayMobile = trim((string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? '')); + $gatewayDob = trim((string) ($profile['dob'] ?? $payload['dob'] ?? '')); + $gatewayGender = trim((string) ($profile['gender'] ?? $payload['gender'] ?? '')); + $gatewayPhoto = trim((string) ($profile['profilePhoto'] ?? $payload['profilePhoto'] ?? '')); + + $patient = $this->findPatientForAbhaProfile($requestedPid, $abhaDigits, $gatewayMobile); + $patientId = (int) ($patient['id'] ?? 0); + $before = $patient !== null ? $this->buildPatientSnapshot($patient) : null; + + $photoMeta = [ + 'saved' => false, + 'path' => '', + 'error' => '', + ]; + + if ($patientId > 0 && $this->db->tableExists('patient_master')) { + $updates = []; + + if ($abhaDigits !== '') { + $abhaField = $this->resolvePatientAbhaIdField(); + if ($abhaField !== null) { + $updates[$abhaField] = $abhaDigits; + } + } + + $pmFields = $this->db->getFieldNames('patient_master') ?? []; + if ($abhaAddress !== '' && in_array('abha_address', $pmFields, true)) { + $updates['abha_address'] = $abhaAddress; + } + if (in_array('abdm_linked_at', $pmFields, true) && ($abhaDigits !== '' || $abhaAddress !== '')) { + $updates['abdm_linked_at'] = date('Y-m-d H:i:s'); + } + + if ($updates !== []) { + $this->db->table('patient_master')->where('id', $patientId)->update($updates); + } + + if ($gatewayPhoto !== '') { + $photoMeta = $this->saveGatewayProfilePhotoToPatient($patientId, $gatewayPhoto); + } + } + + $afterPatient = null; + if ($patientId > 0 && $this->db->tableExists('patient_master')) { + $afterPatient = $this->db->table('patient_master')->where('id', $patientId)->get()->getRowArray(); + } + $after = $afterPatient !== null ? $this->buildPatientSnapshot($afterPatient) : $before; + + $result['gateway_user'] = [ + 'name' => $gatewayName, + 'mobile' => $gatewayMobile, + 'dob' => $gatewayDob, + 'gender' => $gatewayGender, + 'abha_number' => $abhaDigits, + 'abha_address' => $abhaAddress, + 'photo_base64' => $gatewayPhoto, + ]; + + $result['hms_patient'] = [ + 'requested_patient_id' => $requestedPid > 0 ? $requestedPid : null, + 'matched_patient_id' => $patientId > 0 ? $patientId : null, + 'before' => $before, + 'after' => $after, + 'comparison' => [ + 'name_match_before' => $before !== null + ? mb_strtolower(trim((string) ($before['name'] ?? ''))) === mb_strtolower($gatewayName) + : null, + 'name_match_after' => $after !== null + ? mb_strtolower(trim((string) ($after['name'] ?? ''))) === mb_strtolower($gatewayName) + : null, + 'mobile_match_before' => $before !== null + ? preg_replace('/\D/', '', (string) ($before['mobile'] ?? '')) === preg_replace('/\D/', '', $gatewayMobile) + : null, + 'mobile_match_after' => $after !== null + ? preg_replace('/\D/', '', (string) ($after['mobile'] ?? '')) === preg_replace('/\D/', '', $gatewayMobile) + : null, + 'abha_match_before' => $before !== null + ? preg_replace('/\D/', '', (string) ($before['abha_id'] ?? '')) === $abhaDigits + : null, + 'abha_match_after' => $after !== null + ? preg_replace('/\D/', '', (string) ($after['abha_id'] ?? '')) === $abhaDigits + : null, + 'dob_match_before' => $before !== null + ? trim((string) ($before['dob'] ?? '')) === trim($gatewayDob) + : null, + 'dob_match_after' => $after !== null + ? trim((string) ($after['dob'] ?? '')) === trim($gatewayDob) + : null, + 'gender_match_before' => $before !== null + ? $this->normalizeGenderForCompare((string) ($before['gender'] ?? '')) === $this->normalizeGenderForCompare($gatewayGender) + : null, + 'gender_match_after' => $after !== null + ? $this->normalizeGenderForCompare((string) ($after['gender'] ?? '')) === $this->normalizeGenderForCompare($gatewayGender) + : null, + ], + 'photo_update' => $photoMeta, + ]; + + return $result; + } + + /** + * @param array $payload + * @return array + */ + private function pickGatewayAbhaProfile(array $payload): array + { + if (isset($payload['gateway_patient']) && is_array($payload['gateway_patient'])) { + return $payload['gateway_patient']; + } + if (isset($payload['ABHAProfile']) && is_array($payload['ABHAProfile'])) { + return $payload['ABHAProfile']; + } + if (isset($payload['profile']) && is_array($payload['profile'])) { + return $payload['profile']; + } + if (isset($payload['accounts'][0]) && is_array($payload['accounts'][0])) { + return $payload['accounts'][0]; + } + + return []; + } + + /** + * @return array|null + */ + private function findPatientForAbhaProfile(int $requestedPid, string $abhaDigits, string $mobile): ?array + { + if (! $this->db->tableExists('patient_master')) { + return null; + } + + if ($requestedPid > 0) { + $row = $this->db->table('patient_master')->where('id', $requestedPid)->get()->getRowArray(); + if ($row !== null) { + return $row; + } + } + + $abhaField = $this->resolvePatientAbhaIdField(); + if ($abhaDigits !== '' && $abhaField !== null) { + $row = $this->db->table('patient_master')->where($abhaField, $abhaDigits)->get()->getRowArray(); + if ($row !== null) { + return $row; + } + } + + $mobileDigits = preg_replace('/\D/', '', $mobile); + if ($mobileDigits !== '') { + $row = $this->db->table('patient_master')->where('mphone1', $mobileDigits)->get()->getRowArray(); + if ($row !== null) { + return $row; + } + } + + return null; + } + + /** + * @param array $row + * @return array + */ + private function buildPatientSnapshot(array $row): array + { + $abhaField = $this->resolvePatientAbhaIdField(); + $abhaId = $abhaField !== null ? trim((string) ($row[$abhaField] ?? '')) : ''; + $abhaAddress = trim((string) ($row['abha_address'] ?? '')); + + return [ + 'id' => (int) ($row['id'] ?? 0), + 'p_code' => trim((string) ($row['p_code'] ?? '')), + 'name' => trim((string) ($row['p_fname'] ?? '')), + 'mobile' => trim((string) ($row['mphone1'] ?? '')), + 'dob' => trim((string) ($row['dob'] ?? '')), + 'gender' => trim((string) ($row['gender'] ?? '')), + 'abha_id' => $abhaId, + 'abha_address' => $abhaAddress, + 'profile_picture' => trim((string) ($row['profile_picture'] ?? '')), + 'profile_file_id' => (int) ($row['profile_file_id'] ?? 0), + ]; + } + + /** + * @return array{saved:bool,path:string,error:string} + */ + private function saveGatewayProfilePhotoToPatient(int $pid, string $photoBase64): array + { + $raw = trim($photoBase64); + if ($pid <= 0 || $raw === '') { + return ['saved' => false, 'path' => '', 'error' => 'Missing patient id or photo.']; + } + + $mime = 'image/jpeg'; + $encoded = $raw; + if (str_starts_with($raw, 'data:image')) { + $parts = explode('base64,', $raw, 2); + if (count($parts) !== 2) { + return ['saved' => false, 'path' => '', 'error' => 'Invalid image data URI.']; + } + if (preg_match('/data:(image\/[a-zA-Z0-9.+-]+);/i', $parts[0], $m) === 1) { + $mime = strtolower((string) ($m[1] ?? 'image/jpeg')); + } + $encoded = $parts[1]; + } + + $binary = base64_decode($encoded, true); + if ($binary === false) { + return ['saved' => false, 'path' => '', 'error' => 'Unable to decode photo base64.']; + } + + $ext = match ($mime) { + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + default => 'jpg', + }; + + $uploadPath = rtrim(FCPATH, '\\/') . '/uploads/patient'; + if (! is_dir($uploadPath) && ! mkdir($uploadPath, 0755, true) && ! is_dir($uploadPath)) { + return ['saved' => false, 'path' => '', 'error' => 'Unable to create upload directory.']; + } + + $fileName = 'abha_profile_' . $pid . '_' . time() . '.' . $ext; + $absolutePath = $uploadPath . '/' . $fileName; + if (file_put_contents($absolutePath, $binary) === false) { + return ['saved' => false, 'path' => '', 'error' => 'Unable to write photo file.']; + } + + $publicPath = '/uploads/patient/' . $fileName; + $insertId = $this->insertFileUploadRecordFromData($pid, 'profile', $publicPath, $mime, strlen($binary)); + + $updates = []; + if ($insertId > 0) { + $updates['profile_file_id'] = $insertId; + } + if ($this->db->fieldExists('profile_picture', 'patient_master')) { + $updates['profile_picture'] = $publicPath; + } + + if ($updates !== []) { + $this->db->table('patient_master')->where('id', $pid)->update($updates); + } + + return ['saved' => true, 'path' => $publicPath, 'error' => '']; + } + + private function normalizeGenderForCompare(string $value): string + { + $v = strtoupper(trim($value)); + if ($v === '1' || $v === 'M' || $v === 'MALE') { + return 'M'; + } + if ($v === '2' || $v === 'F' || $v === 'FEMALE') { + return 'F'; + } + if ($v === '3' || $v === 'O' || $v === 'OTHER') { + return 'O'; + } + + return $v; + } + private function parseDate(?string $date): ?string { if ($date === null || trim($date) === '') { diff --git a/app/Controllers/Setting/AbdmGatewaySettings.php b/app/Controllers/Setting/AbdmGatewaySettings.php index 17c08e0..ea2c030 100644 --- a/app/Controllers/Setting/AbdmGatewaySettings.php +++ b/app/Controllers/Setting/AbdmGatewaySettings.php @@ -45,7 +45,7 @@ class AbdmGatewaySettings extends BaseController } $gwUrl = trim((string) $this->request->getPost('gateway_url')); - $token = trim((string) $this->request->getPost('api_token')); + $token = $this->sanitizeBearerToken((string) $this->request->getPost('api_token')); $hfrId = trim((string) $this->request->getPost('hfr_id')); $hmsName = trim((string) $this->request->getPost('hms_name')); @@ -118,7 +118,7 @@ class AbdmGatewaySettings extends BaseController // Use posted values first, then stored settings $gwUrl = trim((string) $this->request->getPost('gateway_url')); - $token = trim((string) $this->request->getPost('api_token')); + $token = $this->sanitizeBearerToken((string) $this->request->getPost('api_token')); $hfrId = trim((string) $this->request->getPost('hfr_id')); if ($gwUrl === '') { @@ -412,4 +412,16 @@ class AbdmGatewaySettings extends BaseController } return mb_substr($key, 0, 4) . str_repeat('*', mb_strlen($key) - 8) . mb_substr($key, -4); } + + private function sanitizeBearerToken(string $token): string + { + $token = trim($token); + if ($token === '') { + return ''; + } + if (stripos($token, 'Bearer ') === 0) { + $token = trim(substr($token, 7)); + } + return trim($token, " \t\n\r\0\x0B\"'"); + } } diff --git a/app/Controllers/Storestock.php b/app/Controllers/Storestock.php index a11ac69..594db5f 100644 --- a/app/Controllers/Storestock.php +++ b/app/Controllers/Storestock.php @@ -8,6 +8,7 @@ use Mpdf\Mpdf; class Storestock extends BaseController { protected $db; + protected array $procedureExistsCache = []; public function __construct() { @@ -80,6 +81,96 @@ class Storestock extends BaseController } } + private function normalizeUiDate(string $value): string + { + $value = trim($value); + if ($value === '') { + return ''; + } + + $dt = \DateTime::createFromFormat('d/m/Y', $value); + if ($dt instanceof \DateTime) { + return $dt->format('Y-m-d'); + } + + $dt = \DateTime::createFromFormat('Y-m-d', $value); + return $dt instanceof \DateTime ? $dt->format('Y-m-d') : ''; + } + + private function purchaseReturnInvoiceNoColumn(): string + { + $fields = $this->db->getFieldNames('purchase_return_invoice') ?? []; + if (in_array('p_r_invoice_no', $fields, true)) { + return 'p_r_invoice_no'; + } + + return in_array('Invoice_no', $fields, true) ? 'Invoice_no' : 'id'; + } + + private function fetchPurchaseReturnItems(int $invId): array + { + if ( + $invId <= 0 + || ! $this->db->tableExists('purchase_return_invoice_item') + || ! $this->db->tableExists('purchase_invoice_item') + ) { + return []; + } + + return $this->db->table('purchase_return_invoice_item r') + ->select('p.*, r.purchase_inv_id, r.qty AS r_qty, r.id AS r_id', false) + ->join('purchase_invoice_item p', 'r.purchase_item_id=p.id', 'inner') + ->where('r.purchase_inv_id', $invId) + ->orderBy('r.id', 'ASC') + ->get() + ->getResult(); + } + + private function buildPurchaseReturnItemList(int $invId): string + { + return view('storestock/purchase_return_invoice_item', [ + 'purchase_return_invoice_item' => $this->fetchPurchaseReturnItems($invId), + ]); + } + + private function procedureExists(string $procedureName): bool + { + if (array_key_exists($procedureName, $this->procedureExistsCache)) { + return $this->procedureExistsCache[$procedureName]; + } + + try { + $row = $this->db->query( + 'SELECT ROUTINE_NAME FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA = DATABASE() AND ROUTINE_TYPE = ? AND ROUTINE_NAME = ? LIMIT 1', + ['PROCEDURE', $procedureName] + )->getRowArray(); + + $this->procedureExistsCache[$procedureName] = ! empty($row['ROUTINE_NAME']); + } catch (\Throwable $e) { + $this->procedureExistsCache[$procedureName] = false; + } + + return $this->procedureExistsCache[$procedureName]; + } + + private function runOptionalProcedure(string $procedureName, array $arguments = []): void + { + if (! $this->procedureExists($procedureName)) { + return; + } + + $sql = 'CALL ' . $procedureName . '(' . implode(', ', array_fill(0, count($arguments), '?')) . ')'; + + try { + $this->db->query($sql, $arguments); + } catch (\Throwable $e) { + log_message('warning', 'Optional procedure failed: {procedure} - {message}', [ + 'procedure' => $procedureName, + 'message' => $e->getMessage(), + ]); + } + } + private function buildDashboardInsights(): array { $todayIndents = (int) $this->safeScalar( @@ -618,7 +709,7 @@ class Storestock extends BaseController if ($insert_id > 0) { // Call stored procs $this->db->query("CALL p_stock_update_purchase_id_store({$inv_id})"); - $this->db->query("CALL p_update_med_GST_store({$inv_id})"); + $this->runOptionalProcedure('p_update_med_GST_store', [$inv_id]); $data['inv_stock_item'] = $this->db->query( "SELECT i.*, IFNULL(DATEDIFF(expiry, CURDATE()), 1000) AS no_day, @@ -717,7 +808,7 @@ class Storestock extends BaseController } // Call stored procedure to recalculate GST - $this->db->query("CALL p_update_med_GST_store({$inv_id})"); + $this->runOptionalProcedure('p_update_med_GST_store', [$inv_id]); $data['invoice_stock_master'] = $this->db->query( "SELECT *, (discount_amount + item_discount_amount) AS inv_disc_total, @@ -751,7 +842,7 @@ class Storestock extends BaseController return $err; } - $this->db->query("CALL p_update_med_GST_store({$inv_id})"); + $this->runOptionalProcedure('p_update_med_GST_store', [$inv_id]); $data['inv_items'] = $this->db->query( "SELECT i.indent_id, i.id, i.item_Name, i.formulation, @@ -948,4 +1039,570 @@ class Storestock extends BaseController } return view('storestock/main_store_dashboard'); } + + // ------------------------------------------------------------------------- + // Purchase + // ------------------------------------------------------------------------- + + public function Purchase() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $data['purchase_list'] = []; + if ($this->db->tableExists('purchase_invoice_stock')) { + $data['purchase_list'] = $this->db->table('purchase_invoice_stock p') + ->select("p.id, p.Invoice_no, p.date_of_invoice, DATE_FORMAT(p.date_of_invoice,'%d-%m-%Y') AS str_date_of_invoice, p.sid, IFNULL(s.name_supplier,'-') AS name_supplier, IFNULL(s.short_name,'-') AS short_name, IFNULL(s.gst_no,'-') AS gst_no, IFNULL(p.T_Net_Amount,0) AS tamount, IFNULL(p.inv_status,0) AS inv_status, IFNULL(p.ischallan,0) AS ischallan", false) + ->join('stock_supplier s', 'p.sid=s.sid', 'left') + ->orderBy('p.id', 'DESC') + ->limit(50) + ->get() + ->getResult(); + } + + return view('storestock/purchase', $data); + } + + public function PurchaseInvoice() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $searchRaw = (string) ($this->request->getPost('txtsearch') ?? $this->request->getGet('txtsearch') ?? ''); + $search = trim(preg_replace('/[^A-Za-z0-9_ \-]/', '', $searchRaw)); + + $data['purchase_list'] = []; + if ($this->db->tableExists('purchase_invoice_stock')) { + $builder = $this->db->table('purchase_invoice_stock p') + ->select("p.id, p.Invoice_no, p.date_of_invoice, DATE_FORMAT(p.date_of_invoice,'%d-%m-%Y') AS str_date_of_invoice, p.sid, IFNULL(s.name_supplier,'-') AS name_supplier, IFNULL(s.short_name,'-') AS short_name, IFNULL(s.gst_no,'-') AS gst_no, IFNULL(p.T_Net_Amount,0) AS tamount, IFNULL(p.inv_status,0) AS inv_status, IFNULL(p.ischallan,0) AS ischallan", false) + ->join('stock_supplier s', 'p.sid=s.sid', 'left'); + + if ($search !== '') { + $builder->groupStart(); + $builder->like('p.Invoice_no', $search); + if (ctype_digit($search)) { + $builder->orWhere('p.id', (int) $search); + } + $builder->groupEnd(); + } + + $data['purchase_list'] = $builder->orderBy('p.id', 'DESC')->limit(50)->get()->getResult(); + } + + return view('storestock/purchase_supp_list', $data); + } + + public function PurchaseNew() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $data['supplier_data'] = $this->db->tableExists('stock_supplier') + ? $this->db->table('stock_supplier')->orderBy('name_supplier', 'ASC')->get()->getResult() + : []; + + return view('storestock/purchase_new_invoice', $data); + } + + public function CreatePurchase() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + if (! $this->request->isAJAX() || ! $this->db->tableExists('purchase_invoice_stock')) { + return $this->response->setJSON([ + 'insertid' => 0, + 'show_text' => 'Invalid request', + ]); + } + + $sid = (int) ($this->request->getPost('input_supplier') ?? 0); + $invoiceNo = trim((string) ($this->request->getPost('input_invoicecode') ?? '')); + $invoiceDate = $this->normalizeUiDate((string) ($this->request->getPost('datepicker_invoice') ?? '')); + $billType = (int) ($this->request->getPost('cbo_billtype') ?? 0); + + if ($sid <= 0 || $invoiceNo === '' || $invoiceDate === '') { + return $this->response->setJSON([ + 'insertid' => 0, + 'show_text' => 'Supplier, invoice no and invoice date are required', + ]); + } + + $fields = $this->db->getFieldNames('purchase_invoice_stock') ?? []; + $insert = []; + if (in_array('sid', $fields, true)) { + $insert['sid'] = $sid; + } + if (in_array('Invoice_no', $fields, true)) { + $insert['Invoice_no'] = $invoiceNo; + } + if (in_array('date_of_invoice', $fields, true)) { + $insert['date_of_invoice'] = $invoiceDate; + } + if (in_array('ischallan', $fields, true)) { + $insert['ischallan'] = $billType; + } + if (in_array('inv_status', $fields, true)) { + $insert['inv_status'] = 0; + } + + $this->db->table('purchase_invoice_stock')->insert($insert); + + return $this->response->setJSON([ + 'insertid' => (int) $this->db->insertID(), + 'show_text' => 'Added Successfully', + ]); + } + + public function PurchaseMasterEdit(int $invId) + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $data['supplier_data'] = $this->db->tableExists('stock_supplier') + ? $this->db->table('stock_supplier')->orderBy('name_supplier', 'ASC')->get()->getResult() + : []; + $data['inv_master_data'] = $this->db->query( + 'SELECT p.*, s.name_supplier FROM purchase_invoice_stock p JOIN stock_supplier s ON p.sid=s.sid WHERE p.id = ?', + [$invId] + )->getResult(); + $data['purchase_item'] = $this->db->query( + 'SELECT * FROM purchase_invoice_item_stock WHERE purchase_id = ?', + [$invId] + )->getResult(); + + if (empty($data['inv_master_data'])) { + return $this->response->setStatusCode(404)->setBody('
Purchase invoice not found.
'); + } + + return view('storestock/purchase_invoice_master_edit', $data); + } + + public function PurchaseInvoiceEdit(int $invId) + { + return $this->PurchaseMasterEdit($invId); + } + + public function UpdatePurchase() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + if (! $this->request->isAJAX() || ! $this->db->tableExists('purchase_invoice_stock')) { + return $this->response->setJSON([ + 'insertid' => 0, + 'show_text' => 'Invalid request', + ]); + } + + $recId = (int) ($this->request->getPost('hid_purchaseid') ?? 0); + $sid = (int) ($this->request->getPost('input_supplier') ?? 0); + $invoiceNo = trim((string) ($this->request->getPost('input_invoicecode') ?? '')); + $invoiceDate = $this->normalizeUiDate((string) ($this->request->getPost('datepicker_invoice') ?? '')); + $billType = (int) ($this->request->getPost('cbo_billtype') ?? 0); + + if ($recId <= 0 || $sid <= 0 || $invoiceNo === '' || $invoiceDate === '') { + return $this->response->setJSON([ + 'insertid' => 0, + 'show_text' => 'Please fill supplier, invoice number and invoice date.', + ]); + } + + $update = []; + $fields = $this->db->getFieldNames('purchase_invoice_stock') ?? []; + if (in_array('sid', $fields, true)) { + $update['sid'] = $sid; + } + if (in_array('Invoice_no', $fields, true)) { + $update['Invoice_no'] = $invoiceNo; + } + if (in_array('date_of_invoice', $fields, true)) { + $update['date_of_invoice'] = $invoiceDate; + } + if (in_array('ischallan', $fields, true)) { + $update['ischallan'] = $billType; + } + + if ($update !== []) { + $this->db->table('purchase_invoice_stock')->where('id', $recId)->update($update); + } + + return $this->response->setJSON([ + 'insertid' => 1, + 'show_text' => 'Data Update', + ]); + } + + public function UpdatePurchaseInvoiceStatus(int $purchaseInvId, int $invStatus) + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + if ($this->db->tableExists('purchase_invoice_stock')) { + $this->db->table('purchase_invoice_stock')->where('id', $purchaseInvId)->update([ + 'inv_status' => $invStatus, + ]); + } + + return redirect()->to(site_url('Storestock/PurchaseMasterEdit/' . $purchaseInvId)); + } + + public function purchase_invoice_delete(int $invId) + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + if ($invId <= 0 || ! $this->db->tableExists('purchase_invoice_stock')) { + return $this->response->setJSON([ + 'is_delete' => 0, + 'show_text' => 'Invalid invoice', + ]); + } + + $invoice = $this->db->table('purchase_invoice_stock')->where('id', $invId)->get()->getRow(); + if (! $invoice) { + return $this->response->setJSON([ + 'is_delete' => 0, + 'show_text' => 'Invoice not found', + ]); + } + + if ($this->db->tableExists('purchase_invoice_item_stock')) { + $count = $this->db->table('purchase_invoice_item_stock')->where('purchase_id', $invId)->countAllResults(); + if ($count > 0) { + return $this->response->setJSON([ + 'is_delete' => 0, + 'show_text' => 'Invoice has items, empty the item list first', + ]); + } + } + + if ((float) ($invoice->T_Net_Amount ?? 0) > 0) { + return $this->response->setJSON([ + 'is_delete' => 0, + 'show_text' => 'Cannot delete invoice with amount', + ]); + } + + $this->db->table('purchase_invoice_stock')->where('id', $invId)->delete(); + + return $this->response->setJSON([ + 'is_delete' => 1, + 'show_text' => 'Deleted Successfully', + ]); + } + + public function print_purchase(int $invId) + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $invoice = $this->db->query( + 'SELECT p.id, p.Invoice_no, p.date_of_invoice, DATE_FORMAT(p.date_of_invoice,\'%d/%m/%Y\') AS str_date_of_invoice, p.sid, IFNULL(s.name_supplier,\'-\') AS name_supplier, IFNULL(s.short_name,\'-\') AS short_name, IFNULL(s.gst_no,\'-\') AS gst_no + FROM purchase_invoice_stock p JOIN stock_supplier s ON p.sid=s.sid WHERE p.id = ?', + [$invId] + )->getRow(); + + if (! $invoice) { + return $this->response->setStatusCode(404)->setBody('Purchase invoice not found'); + } + + $items = $this->db->query('SELECT * FROM purchase_invoice_item_stock WHERE purchase_id = ? ORDER BY id ASC', [$invId])->getResult(); + + return view('storestock/purchase_print', [ + 'purchase_invoice' => $invoice, + 'purchase_invoice_item' => $items, + ]); + } + + // ------------------------------------------------------------------------- + // Purchase Return + // ------------------------------------------------------------------------- + + public function Purchase_return() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + return view('storestock/purchase_return'); + } + + public function PurchaseReturnNew() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $supplierData = []; + if ($this->db->tableExists('med_supplier')) { + $supplierData = $this->db->query('SELECT * FROM med_supplier ORDER BY name_supplier')->getResult(); + } elseif ($this->db->tableExists('stock_supplier')) { + $supplierData = $this->db->query('SELECT * FROM stock_supplier ORDER BY name_supplier')->getResult(); + } + + return view('storestock/purchase_return_new', [ + 'supplier_data' => $supplierData, + ]); + } + + public function PurchaseReturnInvoice() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $rows = []; + if ($this->db->tableExists('purchase_return_invoice')) { + $searchRaw = (string) ($this->request->getPost('txtsearch') ?? $this->request->getGet('txtsearch') ?? ''); + $search = preg_replace('/[^A-Za-z0-9_ \-]/', '', trim($searchRaw)); + $invNoCol = $this->purchaseReturnInvoiceNoColumn(); + + $builder = $this->db->table('purchase_return_invoice p') + ->select("p.id, p.{$invNoCol} AS p_r_invoice_no, p.date_of_invoice, DATE_FORMAT(p.date_of_invoice,'%d-%m-%Y') AS str_date_of_invoice, p.sid, IFNULL(s.name_supplier,'-') AS name_supplier, IFNULL(s.short_name,'-') AS short_name", false) + ->join('med_supplier s', 'p.sid=s.sid', 'left'); + + if ($search !== '') { + $builder->groupStart(); + $builder->like("p.{$invNoCol}", $search); + if (ctype_digit($search)) { + $builder->orWhere('p.id', (int) $search); + } + $builder->groupEnd(); + } + + $rows = $builder->orderBy('p.id', 'DESC')->limit(50)->get()->getResult(); + } + + return view('storestock/purchase_return_supplier_list', [ + 'purchase_return_invoice' => $rows, + ]); + } + + public function PurchaseReturnInvoiceEdit(int $invId) + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + if ($invId <= 0 || ! $this->db->tableExists('purchase_return_invoice')) { + return $this->response->setStatusCode(404)->setBody('
Purchase return invoice not found.
'); + } + + $invNoCol = $this->purchaseReturnInvoiceNoColumn(); + $invoice = $this->db->table('purchase_return_invoice p') + ->select("p.id, p.{$invNoCol} AS Invoice_no, p.date_of_invoice, DATE_FORMAT(p.date_of_invoice,'%d/%m/%Y') AS str_date_of_invoice, p.sid, p.status AS inv_status, IFNULL(s.name_supplier,'-') AS name_supplier, IFNULL(s.short_name,'-') AS short_name, IFNULL(s.gst_no,'-') AS gst_no", false) + ->join('med_supplier s', 'p.sid=s.sid', 'left') + ->where('p.id', $invId) + ->get() + ->getRow(); + + if (! $invoice) { + return $this->response->setStatusCode(404)->setBody('
Purchase return invoice not found.
'); + } + + $items = $this->fetchPurchaseReturnItems($invId); + + return view('storestock/purchase_return_invoice_edit', [ + 'purchase_return_invoice' => $invoice, + 'purchase_return_invoice_item' => $items, + 'content' => $this->buildPurchaseReturnItemList($invId), + ]); + } + + public function PurchaseReturn_invoice_item_list(int $invId) + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $invoice = null; + if ($invId > 0 && $this->db->tableExists('purchase_return_invoice')) { + $invoice = $this->db->table('purchase_return_invoice')->where('id', $invId)->get()->getRow(); + } + + return view('storestock/purchase_return_invoice_item', [ + 'purchase_return_invoice_item' => $this->fetchPurchaseReturnItems($invId), + 'purchase_return_invoice' => $invoice ? [$invoice] : [], + ]); + } + + public function create_purchase_return() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + if (! $this->request->isAJAX() || ! $this->db->tableExists('purchase_return_invoice')) { + return $this->response->setJSON([ + 'insertid' => 0, + 'show_text' => 'Invalid request', + ]); + } + + $sid = (int) ($this->request->getPost('input_supplier') ?? 0); + $invoiceDate = $this->normalizeUiDate((string) ($this->request->getPost('datepicker_invoice') ?? '')); + + if ($sid <= 0 || $invoiceDate === '') { + return $this->response->setJSON([ + 'insertid' => 0, + 'show_text' => 'Supplier and date are required', + ]); + } + + $fields = $this->db->getFieldNames('purchase_return_invoice') ?? []; + $insert = []; + if (in_array('sid', $fields, true)) { + $insert['sid'] = $sid; + } + if (in_array('date_of_invoice', $fields, true)) { + $insert['date_of_invoice'] = $invoiceDate; + } + if (in_array('status', $fields, true)) { + $insert['status'] = 0; + } + + $this->db->table('purchase_return_invoice')->insert($insert); + $insertId = (int) $this->db->insertID(); + + if ($insertId > 0 && in_array('p_r_invoice_no', $fields, true)) { + $invoiceNo = 'PR' . date('ym') . str_pad((string) ($insertId % 1000), 3, '0', STR_PAD_LEFT); + $this->db->table('purchase_return_invoice')->where('id', $insertId)->update(['p_r_invoice_no' => $invoiceNo]); + } + + return $this->response->setJSON([ + 'insertid' => $insertId, + 'show_text' => $insertId > 0 ? 'Added Successfully' : 'Unable to create purchase return invoice', + ]); + } + + public function add_remove_item() + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $invId = (int) ($this->request->getPost('inv_id') ?? 0); + $itemId = (int) ($this->request->getPost('itemid') ?? 0); + $qty = (float) ($this->request->getPost('rqty') ?? 0); + $batchNo = trim((string) ($this->request->getPost('rbatch_no') ?? '')); + $expiry = trim((string) ($this->request->getPost('rexpiry_dt') ?? '')); + + if ($invId <= 0 || $itemId <= 0 || $qty <= 0 || ! $this->db->tableExists('purchase_return_invoice_item') || ! $this->db->tableExists('purchase_invoice_item')) { + return $this->response->setJSON(['update' => 0, 'msg_text' => 'Invalid return input', 'content' => '']); + } + + $sourceItem = $this->db->table('purchase_invoice_item')->where('id', $itemId)->get()->getRow(); + if (! $sourceItem) { + return $this->response->setJSON(['update' => 0, 'msg_text' => 'No item found', 'content' => '']); + } + + $insert = []; + $fields = $this->db->getFieldNames('purchase_return_invoice_item') ?? []; + if (in_array('purchase_inv_id', $fields, true)) { + $insert['purchase_inv_id'] = $invId; + } + if (in_array('purchase_item_id', $fields, true)) { + $insert['purchase_item_id'] = $itemId; + } + if (in_array('item_code', $fields, true)) { + $insert['item_code'] = $sourceItem->item_code ?? 0; + } + if (in_array('Item_name', $fields, true)) { + $insert['Item_name'] = $sourceItem->Item_name ?? ($sourceItem->item_name ?? ''); + } + if (in_array('batch_no_r', $fields, true)) { + $insert['batch_no_r'] = $batchNo; + } + if (in_array('expiry_date_r', $fields, true)) { + $insert['expiry_date_r'] = $expiry !== '' ? $expiry : null; + } + if (in_array('qty', $fields, true)) { + $insert['qty'] = $qty; + } + + $this->db->table('purchase_return_invoice_item')->insert($insert); + + return $this->response->setJSON([ + 'update' => 1, + 'msg_text' => 'Item Added', + 'content' => $this->buildPurchaseReturnItemList($invId), + ]); + } + + public function remove_item_invoice(int $itemId) + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + $row = null; + if ($itemId > 0 && $this->db->tableExists('purchase_return_invoice_item')) { + $row = $this->db->table('purchase_return_invoice_item')->where('id', $itemId)->get()->getRow(); + } + + if (! $row) { + return $this->response->setJSON(['update' => 0, 'msg_text' => 'No item found', 'content' => '']); + } + + $invId = (int) ($row->purchase_inv_id ?? 0); + $this->db->table('purchase_return_invoice_item')->where('id', $itemId)->delete(); + + return $this->response->setJSON([ + 'update' => 1, + 'msg_text' => 'Item Removed', + 'content' => $this->buildPurchaseReturnItemList($invId), + ]); + } + + public function print_purchase_return(int $invId) + { + if ($err = $this->ensureStoreAccess()) { + return $err; + } + + if ($invId <= 0 || ! $this->db->tableExists('purchase_return_invoice')) { + return $this->response->setStatusCode(404)->setBody('Purchase return invoice not found'); + } + + $invNoCol = $this->purchaseReturnInvoiceNoColumn(); + $invoice = $this->db->table('purchase_return_invoice p') + ->select("p.id, p.{$invNoCol} AS Invoice_no, p.date_of_invoice, DATE_FORMAT(p.date_of_invoice,'%d/%m/%Y') AS str_date_of_invoice, p.sid, IFNULL(s.name_supplier,'-') AS name_supplier, IFNULL(s.short_name,'-') AS short_name, IFNULL(s.gst_no,'-') AS gst_no", false) + ->join('med_supplier s', 'p.sid=s.sid', 'left') + ->where('p.id', $invId) + ->get() + ->getRow(); + + if (! $invoice) { + return $this->response->setStatusCode(404)->setBody('Purchase return invoice not found'); + } + + $items = $this->fetchPurchaseReturnItems($invId); + $html = view('storestock/purchase_return_invoice_item_print', [ + 'purchase_return_invoice' => [$invoice], + 'purchase_return_invoice_item' => $items, + ]); + + $mpdf = new Mpdf([ + 'mode' => 'utf-8', + 'format' => 'A4', + 'tempDir' => WRITEPATH . 'cache/mpdf', + ]); + $mpdf->showWatermarkText = false; + $mpdf->WriteHTML($html); + + $filename = 'Return_Invoice-' . $invId . '-' . date('YmdHis') . '.pdf'; + return $this->response + ->setHeader('Content-Type', 'application/pdf') + ->setBody($mpdf->Output($filename, 'S')); + } } diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index 9644cf8..e55e0ce 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -28,7 +28,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $config = config('AbdmConnector'); $this->baseUrl = rtrim((string) ($config->eatriaBridgeUrl ?? 'https://abdm-bridge.e-atria.in/api'), '/'); - $this->token = (string) ($config->eatriaBridgeToken ?? ''); + $this->token = $this->sanitizeBearerToken((string) ($config->eatriaBridgeToken ?? '')); $this->hfrId = ''; $this->timeoutSec = (int) ($config->eatriaBridgeTimeoutSec ?? 30); @@ -41,14 +41,19 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface if ($db->tableExists('hospital_setting')) { $rows = $db->table('hospital_setting') ->select('s_name, s_value') - ->whereIn('s_name', ['EATRIA_BRIDGE_TOKEN', 'EATRIA_BRIDGE_URL', 'ABDM_HFR_ID']) + ->whereIn('s_name', ['EATRIA_BRIDGE_TOKEN', 'ABDM_BRIDGE_TOKEN', 'EATRIA_BRIDGE_URL', 'ABDM_HFR_ID']) ->get() ->getResultArray(); $dbSettings = array_column($rows, 's_value', 's_name'); - if (! empty($dbSettings['EATRIA_BRIDGE_TOKEN'])) { - $this->token = trim($dbSettings['EATRIA_BRIDGE_TOKEN']); + $dbToken = (string) ($dbSettings['EATRIA_BRIDGE_TOKEN'] ?? ''); + if ($dbToken === '') { + // Backward compatibility: older setups may store the key under ABDM_BRIDGE_TOKEN. + $dbToken = (string) ($dbSettings['ABDM_BRIDGE_TOKEN'] ?? ''); + } + if ($dbToken !== '') { + $this->token = $this->sanitizeBearerToken($dbToken); } if (! empty($dbSettings['EATRIA_BRIDGE_URL'])) { $this->baseUrl = rtrim(trim($dbSettings['EATRIA_BRIDGE_URL']), '/'); @@ -67,6 +72,21 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface return 'eatria_bridge'; } + private function sanitizeBearerToken(string $token): string + { + $token = trim($token); + if ($token === '') { + return ''; + } + + // Operators sometimes paste "Bearer " into settings. + if (stripos($token, 'Bearer ') === 0) { + $token = trim(substr($token, 7)); + } + + return trim($token, " \t\n\r\0\x0B\"'"); + } + // ------------------------------------------------------------------------- // Internal HTTP helper // ------------------------------------------------------------------------- @@ -221,6 +241,9 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface public function validateAbha(string $abhaId, array $fullPayload = []): array { $body = $fullPayload !== [] ? $fullPayload : ['abha_id' => $abhaId]; + if ($this->hfrId !== '' && empty($body['hfr_id'])) { + $body['hfr_id'] = $this->hfrId; + } return $this->post('/v3/abha/validate', $body); } @@ -234,6 +257,9 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $body = [ 'aadhaar' => (string) ($payload['aadhaar'] ?? $payload['loginId'] ?? ''), ]; + if ($this->hfrId !== '' && empty($body['hfr_id'])) { + $body['hfr_id'] = $this->hfrId; + } return $this->post('/v3/abha/aadhaar/generate-otp', $body); } @@ -250,6 +276,9 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface if (preg_match('/^\d{10}$/', $mobile)) { $body['mobile'] = $mobile; } + if ($this->hfrId !== '' && empty($body['hfr_id'])) { + $body['hfr_id'] = $this->hfrId; + } return $this->post('/v3/abha/aadhaar/verify-otp', $body); } @@ -260,6 +289,9 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $body = [ 'mobile' => (string) ($payload['mobile'] ?? $payload['loginId'] ?? ''), ]; + if ($this->hfrId !== '' && empty($body['hfr_id'])) { + $body['hfr_id'] = $this->hfrId; + } return $this->post('/v3/abha/mobile/generate-otp', $body); } @@ -270,6 +302,9 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface 'txnId' => (string) ($payload['txnId'] ?? $payload['txn_id'] ?? ''), 'otp' => (string) ($payload['otp'] ?? ''), ]; + if ($this->hfrId !== '' && empty($body['hfr_id'])) { + $body['hfr_id'] = $this->hfrId; + } return $this->post('/v3/abha/mobile/verify-otp', $body); } diff --git a/app/Views/billing/opd_invoice_V.php b/app/Views/billing/opd_invoice_V.php index a095fde..7945f21 100644 --- a/app/Views/billing/opd_invoice_V.php +++ b/app/Views/billing/opd_invoice_V.php @@ -296,6 +296,12 @@ $paymentHistoryRows = $payment_history_rows ?? []; + apointment_date ?? ''); + $appointmentTimestamp = $appointmentDateRaw !== '' ? strtotime($appointmentDateRaw) : false; + $appointmentDateValue = $appointmentTimestamp !== false ? date('Y-m-d', $appointmentTimestamp) : ''; + $appointmentDateDisplay = $appointmentTimestamp !== false ? date('d/m/Y', $appointmentTimestamp) : ''; + ?>
@@ -306,8 +312,8 @@ $paymentHistoryRows = $payment_history_rows ?? [];
- - + +
diff --git a/app/Views/billing/opd_prescription_basic.php b/app/Views/billing/opd_prescription_basic.php index 359e377..fd3f694 100644 --- a/app/Views/billing/opd_prescription_basic.php +++ b/app/Views/billing/opd_prescription_basic.php @@ -1576,6 +1576,8 @@ var legacyInvestigationProfileList = []; var legacyInvestigationShortTests = []; var investigationBatchActive = false; + var investigationListLoading = false; + var investigationListPending = false; var activeMedicineScope = 'all'; var medicineSuggestRows = []; var medicineSearchCache = {}; @@ -6589,37 +6591,6 @@ return (name || '').toString().trim().toLowerCase(); } - function addInvestigationEntry(name, code, done) { - name = (name || '').trim(); - code = (code || '').trim(); - if (!name) { - if (typeof done === 'function') { - done(false); - } - return; - } - - ensureSession(function(sid) { - apiPost('', { - opd_id: $('#opd_id').val(), - opd_session_id: sid, - investigation_code: code, - investigation_name: name - }, function(data) { - if (data.update == 1) { - $('#opd_session_id').val(data.opd_session_id || sid); - if (typeof done === 'function') { - done(true); - } - return; - } - if (typeof done === 'function') { - done(false); - } - }); - }); - } - function batchAddInvestigations(tests) { tests = Array.isArray(tests) ? tests : []; if (!tests.length) { @@ -6663,41 +6634,56 @@ } investigationBatchActive = true; - - var idx = 0; - var added = 0; - function next() { - if (idx >= queue.length) { + ensureSession(function(sid) { + apiPost('', { + opd_id: $('#opd_id').val(), + opd_session_id: sid, + tests: queue + }, function(data) { investigationBatchActive = false; - loadInvestigationList(); - $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text(added + ' test(s) added.'); - markDirty('Investigation shortcuts applied'); - return; - } - var row = queue[idx++] || {}; - var key = normalizeInvestigationKey(row.name || ''); - addInvestigationEntry((row.name || '').toString(), (row.code || '').toString(), function(ok) { - if (ok) { - added++; - if (key) { - existing[key] = true; + if (data.update == 1) { + $('#opd_session_id').val(data.opd_session_id || sid); + loadInvestigationList(); + var addedCount = parseInt(data.added_count || '0', 10); + var skippedCount = parseInt(data.skipped_count || '0', 10); + var msg = addedCount + ' test(s) added.'; + if (skippedCount > 0) { + msg += ' ' + skippedCount + ' duplicate/empty skipped.'; } + $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text(msg); + if (addedCount > 0) { + markDirty('Investigation shortcuts applied'); + } + return; } - next(); + $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text(data.error_text || 'Unable to add investigations.'); }); - } - next(); + }); } function loadInvestigationList() { var opdId = $('#opd_id').val(); var sid = parseInt($('#opd_session_id').val() || '0', 10); if (sid <= 0) { + investigationListLoading = false; + investigationListPending = false; renderInvestigation([]); return; } + + if (investigationListLoading) { + investigationListPending = true; + return; + } + + investigationListLoading = true; apiGet('/' + opdId + '/' + sid, function(data) { + investigationListLoading = false; renderInvestigation(data.rows || []); + if (investigationListPending) { + investigationListPending = false; + loadInvestigationList(); + } }); } @@ -6753,15 +6739,11 @@ return; } - addInvestigationEntry(name, code, function(ok) { - if (ok) { - $('#investigation_name').val(''); - if ($.fn && $.fn.select2 && $('#investigation_name_select2').length) { - $('#investigation_name_select2').val(null).trigger('change'); - } - loadInvestigationList(); - } - }); + batchAddInvestigationRows([{ name: name, code: code }]); + $('#investigation_name').val(''); + if ($.fn && $.fn.select2 && $('#investigation_name_select2').length) { + $('#investigation_name_select2').val(null).trigger('change'); + } }); $('#btn_clear_investigation').on('click', function() { @@ -6777,24 +6759,23 @@ return; } - var index = 0; - function removeNext() { - if (index >= ids.length) { + apiPost('', { + ids: ids + }, function(data) { + if (data.update == 1) { loadInvestigationList(); $('.jsError').removeClass('text-danger text-muted').addClass('text-success').text('All investigations removed.'); markDirty('Investigation list cleared'); return; } - - var id = ids[index++]; - apiPost('/' + id, {}, function() { - removeNext(); - }); - } - removeNext(); + $('.jsError').removeClass('text-success text-muted').addClass('text-danger').text(data.error_text || 'Unable to remove investigations.'); + }); }); - $(document).on('click', '.inv-quick-chip', function() { + $(document).off('click', '.inv-quick-chip'); + $(document).off('click', '.inv-profile-chip'); + + $(document).off('click.invQuickChip', '.inv-quick-chip').on('click.invQuickChip', '.inv-quick-chip', function() { var test = ($(this).data('test') || '').toString().trim(); var code = ($(this).data('code') || '').toString().trim(); if (!test) { @@ -6807,7 +6788,7 @@ batchAddInvestigations([test]); }); - $(document).on('click', '.inv-profile-chip', function() { + $(document).off('click.invProfileChip', '.inv-profile-chip').on('click.invProfileChip', '.inv-profile-chip', function() { var key = ($(this).data('profile') || '').toString().trim(); if (legacyInvestigationProfiles[key] && legacyInvestigationProfiles[key].length) { batchAddInvestigationRows(legacyInvestigationProfiles[key]); @@ -6947,7 +6928,7 @@ loadMedicineDoseMasters(); $('#advise_investigation_notes').val($('#investigation').val() || ''); - $(document).on('click', '.btn-del-invest', function() { + $(document).off('click.investRemoveSingle', '.btn-del-invest').on('click.investRemoveSingle', '.btn-del-invest', function() { var id = $(this).data('id'); apiPost('/' + id, {}, function() { loadInvestigationList(); diff --git a/app/Views/partials/abha_otp_modal.php b/app/Views/partials/abha_otp_modal.php index c63767e..20f35c5 100644 --- a/app/Views/partials/abha_otp_modal.php +++ b/app/Views/partials/abha_otp_modal.php @@ -170,11 +170,37 @@ } function clearMsg(id) { document.getElementById(id).innerHTML = ''; } + function parseXhrJson(xhr) { + if (!xhr) return {}; + if (xhr.responseJSON && typeof xhr.responseJSON === 'object') { + return xhr.responseJSON; + } + var raw = xhr.responseText || ''; + if (raw === '') return {}; + try { + return JSON.parse(raw); + } catch (e) { + return {}; + } + } + + function pickErrorMessage(res, fallbackText) { + var payload = (res && (res.data || res)) || {}; + var profile = payload.profile || payload.ABHAProfile || {}; + return ( + (res && (res.error_text || res.message || res.error)) || + payload.message || + profile.message || + profile.code || + fallbackText + ); + } + function postAbdm(url, data, cb) { var csrf = csrfPair(); data[csrf.name] = csrf.value; $.post(url, data, function (res) { updateCsrf(res); cb(res || {}); }, 'json') - .fail(function (xhr) { cb(xhr && xhr.responseJSON ? xhr.responseJSON : {}); }); + .fail(function (xhr) { cb(parseXhrJson(xhr)); }); } function disableBtn(id) { $('#' + id).prop('disabled', true); } @@ -256,7 +282,7 @@ AADHAAR OTP FLOW ================================================================ */ - $('#btn_aadhaar_send_otp').on('click', function () { + $('#btn_aadhaar_send_otp').off('click.abhaOtp').on('click.abhaOtp', function () { clearMsg('aadhaar_step1_msg'); var aadhaar = $('#abha_aadhaar_input').val().trim(); if (!/^\d{12}$/.test(aadhaar)) { @@ -284,19 +310,19 @@ $('#aadhaar-step2').show(); setTimeout(function () { $('#abha_aadhaar_otp').trigger('focus'); }, 100); } else { - var errMsg = res.error_text || res.message || res.error || 'Failed to send OTP. Please try again.'; + var errMsg = pickErrorMessage(res, 'Failed to send OTP. Please try again.'); showMsg('aadhaar_step1_msg', 'danger', '' + $('
').text(errMsg).html()); } }); }); - $('#btn_aadhaar_back').on('click', function () { + $('#btn_aadhaar_back').off('click.abhaOtp').on('click.abhaOtp', function () { $('#aadhaar-step2').hide(); $('#aadhaar-step1').show(); clearMsg('aadhaar_step2_msg'); }); - $('#btn_aadhaar_resend').on('click', function () { + $('#btn_aadhaar_resend').off('click.abhaOtp').on('click.abhaOtp', function () { var aadhaar = $('#abha_aadhaar_input').val().trim(); if (!/^\d{12}$/.test(aadhaar)) { showMsg('aadhaar_step2_msg', 'warning', 'Go back and re-enter the Aadhaar number.'); @@ -320,13 +346,13 @@ showMsg('aadhaar_step2_msg', 'success', 'OTP resent. Enter the new OTP above.'); setTimeout(function () { $('#abha_aadhaar_otp').trigger('focus'); }, 100); } else { - var errMsg = res.error_text || res.message || res.error || 'Resend failed.'; + var errMsg = pickErrorMessage(res, 'Resend failed.'); showMsg('aadhaar_step2_msg', 'danger', '' + $('
').text(errMsg).html()); } }); }); - $('#btn_aadhaar_verify_otp').on('click', function () { + $('#btn_aadhaar_verify_otp').off('click.abhaOtp').on('click.abhaOtp', function () { clearMsg('aadhaar_step2_msg'); var otp = $('#abha_aadhaar_otp').val().trim(); var mobile = $('#abha_aadhaar_mobile').val().trim(); @@ -345,7 +371,7 @@ disableBtn('btn_aadhaar_verify_otp'); $('#btn_aadhaar_verify_otp').html('Verifying…'); - postAbdm('', { txnId: _aadhaarTxnId, otp: otp, mobile: mobile }, function (res) { + postAbdm('', { txnId: _aadhaarTxnId, otp: otp, mobile: mobile, p_id: _patientId }, function (res) { enableBtn('btn_aadhaar_verify_otp'); $('#btn_aadhaar_verify_otp').html('Verify OTP & Link ABHA'); @@ -360,14 +386,14 @@ $('#btn_aadhaar_verify_otp').prop('disabled', true).text('Linked ✓'); setTimeout(function () { bootstrap.Modal.getInstance(document.getElementById('abhaOtpModal')).hide(); }, 1800); } else { - var errMsg = res.error_text || res.message || res.error || 'OTP verification failed.'; + var errMsg = pickErrorMessage(res, 'OTP verification failed.'); showMsg('aadhaar_step2_msg', 'danger', '' + $('
').text(errMsg).html()); } }); }); /* Allow Enter key in OTP field */ - $('#abha_aadhaar_otp').on('keydown', function (e) { + $('#abha_aadhaar_otp').off('keydown.abhaOtp').on('keydown.abhaOtp', function (e) { if (e.key === 'Enter') $('#btn_aadhaar_verify_otp').trigger('click'); }); @@ -375,7 +401,7 @@ MOBILE OTP FLOW ================================================================ */ - $('#btn_mobile_send_otp').on('click', function () { + $('#btn_mobile_send_otp').off('click.abhaOtp').on('click.abhaOtp', function () { clearMsg('mobile_step1_msg'); var mobile = $('#abha_mobile_input').val().trim(); if (!/^\d{10}$/.test(mobile)) { @@ -397,19 +423,19 @@ $('#mobile-step2').show(); setTimeout(function () { $('#abha_mobile_otp').trigger('focus'); }, 100); } else { - var errMsg = res.error_text || res.message || res.error || 'Failed to send OTP.'; + var errMsg = pickErrorMessage(res, 'Failed to send OTP.'); showMsg('mobile_step1_msg', 'danger', '' + $('
').text(errMsg).html()); } }); }); - $('#btn_mobile_back').on('click', function () { + $('#btn_mobile_back').off('click.abhaOtp').on('click.abhaOtp', function () { $('#mobile-step2').hide(); $('#mobile-step1').show(); clearMsg('mobile_step2_msg'); }); - $('#btn_mobile_verify_otp').on('click', function () { + $('#btn_mobile_verify_otp').off('click.abhaOtp').on('click.abhaOtp', function () { clearMsg('mobile_step2_msg'); var otp = $('#abha_mobile_otp').val().trim(); if (!/^\d{6}$/.test(otp)) { @@ -423,7 +449,7 @@ disableBtn('btn_mobile_verify_otp'); $('#btn_mobile_verify_otp').html('Verifying…'); - postAbdm('', { txnId: _mobileTxnId, otp: otp }, function (res) { + postAbdm('', { txnId: _mobileTxnId, otp: otp, p_id: _patientId }, function (res) { enableBtn('btn_mobile_verify_otp'); $('#btn_mobile_verify_otp').html('Verify OTP & Link ABHA'); @@ -438,13 +464,13 @@ $('#btn_mobile_verify_otp').prop('disabled', true).text('Linked ✓'); setTimeout(function () { bootstrap.Modal.getInstance(document.getElementById('abhaOtpModal')).hide(); }, 1800); } else { - var errMsg = res.error_text || res.message || res.error || 'OTP verification failed.'; + var errMsg = pickErrorMessage(res, 'OTP verification failed.'); showMsg('mobile_step2_msg', 'danger', '' + $('
').text(errMsg).html()); } }); }); - $('#abha_mobile_otp').on('keydown', function (e) { + $('#abha_mobile_otp').off('keydown.abhaOtp').on('keydown.abhaOtp', function (e) { if (e.key === 'Enter') $('#btn_mobile_verify_otp').trigger('click'); }); diff --git a/app/Views/storestock/dashboard.php b/app/Views/storestock/dashboard.php index b6be8b9..9f0be4a 100644 --- a/app/Views/storestock/dashboard.php +++ b/app/Views/storestock/dashboard.php @@ -5,10 +5,30 @@ $stats = $stats ?? []; $lists = $lists ?? []; $stockValue = (float) ($stats['stock_value'] ?? 0); ?> -
-

Store Stock Dashboard

-
+

Hospital Stock Control Center

Track indents, monitor stock movement, and jump to master controls from one screen.

@@ -67,14 +87,7 @@ $stockValue = (float) ($stats['stock_value'] ?? 0);
-
-
-

Workspace

-
-
-
-
-
+
@@ -147,5 +160,6 @@ $stockValue = (float) ($stats['stock_value'] ?? 0);
+
diff --git a/app/Views/storestock/dashboard_sub.php b/app/Views/storestock/dashboard_sub.php new file mode 100644 index 0000000..7379c65 --- /dev/null +++ b/app/Views/storestock/dashboard_sub.php @@ -0,0 +1,131 @@ +
+

Hospital Stock Control Center

+

Track indents, monitor stock movement, and jump to master controls from one screen.

+
+ +
+
+ Indents Today + +
+
+ Indents This Month + +
+
+ Low Stock Items + +
+
+ Near Expiry (60 Days) + +
+
+ +
+
+ Pending Purchase Invoices + +
+
+ Approx. Current Stock Value + Rs. +
+
+ + + + + +
+
+
+

Low Stock Alerts

+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
ItemCurrent UnitReorder Threshold
No low-stock items right now.
x
+
+
+
+ +
+
+

Near Expiry Alerts

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + +
Item / BatchExpiryDays LeftCur Packs
No near-expiry batches in next 60 days.
() + + + +
+
+
+
+
\ No newline at end of file diff --git a/app/Views/storestock/day_report.php b/app/Views/storestock/day_report.php index 89a9880..849625f 100644 --- a/app/Views/storestock/day_report.php +++ b/app/Views/storestock/day_report.php @@ -1,40 +1,66 @@ include('storestock/_niceadmin_theme') ?>
-
-
-
-

Day Report - Store

+
+
+
+ Store Stock + Day Report
-
-
- -
-
-
- - -
-
-
-
- - -
-
-
-
- -
-
-
-
-
+
+
+
+
+
+

Day Report - Store

+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+
+
+
+
- \ No newline at end of file diff --git a/app/Views/storestock/purchase_invoice_master_edit.php b/app/Views/storestock/purchase_invoice_master_edit.php new file mode 100644 index 0000000..07cf375 --- /dev/null +++ b/app/Views/storestock/purchase_invoice_master_edit.php @@ -0,0 +1,136 @@ + + + +
Purchase invoice not found.
+ +
+
+
+

Purchase Invoice / Supplier : name_supplier ?? '-')) ?>

+ + Invoice No.: Invoice_no ?? '')) ?> | + Invoice Date: str_date_of_invoice ?? '')) ?> + +
+
+ + + Print +
+
+
+
+ + +
+ + +
+
+ + +
+
+ + inv_status ?? 0)) === 0 ? '' : 'readonly' ?>> +
+
+ + inv_status ?? 0)) === 0 ? '' : 'readonly' ?>> +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#Item NameBatch NoExp.MRPQty.RateAmountDisc.Tax AmountCGSTSGSTNet Amount
No items added yet.
Item_name ?? $row->item_name ?? '')) ?>batch_no ?? '-')) ?>expiry_date ?? '-')) ?>mrp ?? '0')) ?>qty ?? 0))) ?>qty_free ?? 0)) > 0 ? ' + ' . esc((string) ((float) ($row->qty_free ?? 0))) : '' ?>purchase_price ?? '0')) ?>amount ?? '0')) ?>discount ?? $row->disc_amount ?? '0')) ?>taxable_amount ?? '0')) ?>CGST_per ?? '0')) ?>SGST_per ?? '0')) ?>net_amount ?? '0')) ?>
TotalsTaxable_Amt ?? '0')) ?>CGST_Amt ?? '0')) ?>SGST_Amt ?? '0')) ?>T_Net_Amount ?? '0')) ?>
+
+
+
+ + + \ No newline at end of file diff --git a/app/Views/storestock/purchase_new_invoice.php b/app/Views/storestock/purchase_new_invoice.php new file mode 100644 index 0000000..853a434 --- /dev/null +++ b/app/Views/storestock/purchase_new_invoice.php @@ -0,0 +1,60 @@ +
+
+

New Purchase Invoice

+ +
+
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/app/Views/storestock/purchase_print.php b/app/Views/storestock/purchase_print.php new file mode 100644 index 0000000..3d65b79 --- /dev/null +++ b/app/Views/storestock/purchase_print.php @@ -0,0 +1,51 @@ + + + + + + + Purchase Invoice + + + +
Purchase Invoice
+
Supplier: name_supplier ?? '-')) ?> | Invoice No.: Invoice_no ?? '')) ?> | Date: str_date_of_invoice ?? '')) ?>
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#ItemBatchExp.QtyRateAmountNet
Item_name ?? $row->item_name ?? '')) ?>batch_no ?? '-')) ?>expiry_date ?? '-')) ?>qty ?? 0))) ?>purchase_price ?? '0')) ?>amount ?? '0')) ?>net_amount ?? '0')) ?>
+ + \ No newline at end of file diff --git a/app/Views/storestock/purchase_return_invoice_edit.php b/app/Views/storestock/purchase_return_invoice_edit.php new file mode 100644 index 0000000..113b502 --- /dev/null +++ b/app/Views/storestock/purchase_return_invoice_edit.php @@ -0,0 +1,24 @@ + + +
Purchase return invoice not found.
+ +
+
+
+

Purchase Return Invoice / Supplier : name_supplier ?? '-')) ?>

+ + Invoice No.: Invoice_no ?? '')) ?> | + Invoice Date: str_date_of_invoice ?? '')) ?> + +
+
+ + + Print +
+
+
+ +
+
+ \ No newline at end of file diff --git a/app/Views/storestock/purchase_return_invoice_item.php b/app/Views/storestock/purchase_return_invoice_item.php new file mode 100644 index 0000000..4df96c1 --- /dev/null +++ b/app/Views/storestock/purchase_return_invoice_item.php @@ -0,0 +1,28 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
#ItemBatchExpiryQty
No return items found.
Item_name ?? '')) ?>batch_no_r ?? $row->batch_no ?? '-')) ?>expiry_date_r ?? $row->expiry_date ?? '-')) ?>r_qty ?? $row->qty ?? '0')) ?>
+
\ No newline at end of file diff --git a/app/Views/storestock/purchase_return_new.php b/app/Views/storestock/purchase_return_new.php new file mode 100644 index 0000000..ea195a6 --- /dev/null +++ b/app/Views/storestock/purchase_return_new.php @@ -0,0 +1,50 @@ +
+
+

New Purchase Return Invoice

+ +
+
+
+ + +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+ + \ No newline at end of file diff --git a/app/Views/storestock/purchase_supp_list.php b/app/Views/storestock/purchase_supp_list.php new file mode 100644 index 0000000..27f66aa --- /dev/null +++ b/app/Views/storestock/purchase_supp_list.php @@ -0,0 +1,51 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Invoice IDSupplierDateAmountAction
No purchase invoices found.
Invoice_no ?? '')) ?>name_supplier ?? '')) ?>str_date_of_invoice ?? $row->date_of_invoice ?? '')) ?>tamount ?? 0), 2)) ?> + + Print +
+
+ + \ No newline at end of file