feat: update storestock flow and ABDM bridge handling

This commit is contained in:
DevSoftTech Server 2026-06-18 17:38:20 +05:30
parent 1790be3fca
commit 1bac1f6e04
20 changed files with 2264 additions and 196 deletions

View File

@ -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'));

View File

@ -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<string, mixed> $result
* @return array<string, mixed>
*/
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<string, mixed> $payload
* @return array<string, mixed>
*/
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<string, mixed>|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<string, mixed> $row
* @return array<string, mixed>
*/
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) === '') {

View File

@ -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\"'");
}
}

View File

@ -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('<div class="alert alert-warning m-3">Purchase invoice not found.</div>');
}
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('<div class="alert alert-warning m-3">Purchase return invoice not found.</div>');
}
$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('<div class="alert alert-warning m-3">Purchase return invoice not found.</div>');
}
$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'));
}
}

View File

@ -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 <token>" 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);
}

View File

@ -296,6 +296,12 @@ $paymentHistoryRows = $payment_history_rows ?? [];
<?php endif; ?>
</div>
</div>
<?php
$appointmentDateRaw = (string) ($opd_master[0]->apointment_date ?? '');
$appointmentTimestamp = $appointmentDateRaw !== '' ? strtotime($appointmentDateRaw) : false;
$appointmentDateValue = $appointmentTimestamp !== false ? date('Y-m-d', $appointmentTimestamp) : '';
$appointmentDateDisplay = $appointmentTimestamp !== false ? date('d/m/Y', $appointmentTimestamp) : '';
?>
<div class="row g-3 mt-2">
<div class="col-md-3">
<label>Change Doctor</label>
@ -306,8 +312,8 @@ $paymentHistoryRows = $payment_history_rows ?? [];
</select>
</div>
<div class="col-md-3">
<label>Date <?= esc(MysqlDate_to_str($opd_master[0]->apointment_date ?? '')) ?></label>
<input class="form-control datepicker" id="datepicker_opddate" name="datepicker_opddate" type="text" value="<?= esc(MysqlDate_to_str($opd_master[0]->apointment_date ?? '')) ?>" />
<label>Date <?= esc($appointmentDateDisplay) ?></label>
<input class="form-control" id="datepicker_opddate" name="datepicker_opddate" type="date" value="<?= esc($appointmentDateValue) ?>" />
</div>
<div class="col-md-3">
<label>OPD Fee <?= esc($opd_master[0]->opd_fee_gross_amount ?? '') ?> / <?= esc($opd_master[0]->opd_fee_amount ?? '') ?></label>

View File

@ -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('<?= base_url('Opd_prescription/investigation_add') ?>', {
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('<?= base_url('Opd_prescription/investigation_add_bulk') ?>', {
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('<?= base_url('Opd_prescription/investigation_list') ?>/' + 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('<?= base_url('Opd_prescription/investigation_remove_bulk') ?>', {
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('<?= base_url('Opd_prescription/investigation_remove') ?>/' + 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('<?= base_url('Opd_prescription/investigation_remove') ?>/' + id, {}, function() {
loadInvestigationList();

View File

@ -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', '<i class="bi bi-x-circle me-1"></i>' + $('<div>').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', '<i class="bi bi-check-circle me-1"></i>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', '<i class="bi bi-x-circle me-1"></i>' + $('<div>').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('<span class="spinner-border spinner-border-sm me-1"></span>Verifying…');
postAbdm('<?= base_url('billing/patient/abha_aadhaar_verify_otp') ?>', { txnId: _aadhaarTxnId, otp: otp, mobile: mobile }, function (res) {
postAbdm('<?= base_url('billing/patient/abha_aadhaar_verify_otp') ?>', { txnId: _aadhaarTxnId, otp: otp, mobile: mobile, p_id: _patientId }, function (res) {
enableBtn('btn_aadhaar_verify_otp');
$('#btn_aadhaar_verify_otp').html('<i class="bi bi-check-circle me-1"></i>Verify OTP &amp; 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', '<i class="bi bi-x-circle me-1"></i>' + $('<div>').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', '<i class="bi bi-x-circle me-1"></i>' + $('<div>').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('<span class="spinner-border spinner-border-sm me-1"></span>Verifying…');
postAbdm('<?= base_url('billing/patient/abha_mobile_verify_otp') ?>', { txnId: _mobileTxnId, otp: otp }, function (res) {
postAbdm('<?= base_url('billing/patient/abha_mobile_verify_otp') ?>', { txnId: _mobileTxnId, otp: otp, p_id: _patientId }, function (res) {
enableBtn('btn_mobile_verify_otp');
$('#btn_mobile_verify_otp').html('<i class="bi bi-check-circle me-1"></i>Verify OTP &amp; 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', '<i class="bi bi-x-circle me-1"></i>' + $('<div>').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');
});

View File

@ -5,10 +5,30 @@ $stats = $stats ?? [];
$lists = $lists ?? [];
$stockValue = (float) ($stats['stock_value'] ?? 0);
?>
<section class="content-header">
<h1>Store Stock <small>Dashboard</small></h1>
</section>
<section class="content">
<div class="module-shell-head">
<div class="module-shell-title">
<span class="main">Store Stock</span>
<span class="sub" id="subtitle">Dashboard</span>
</div>
<div class="module-nav-tabs">
<a class="tab active" href="javascript:load_form('<?= base_url('Storestock') ?>','Hospital Stock');">
<i class="fa fa-home"></i> Dashboard
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/Indent_List') ?>','Store : Indent');">
<i class="fa fa-shopping-cart"></i> Indent
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/Report_2') ?>','Day Report :Store');">
<i class="fa fa-line-chart"></i> Day Report
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/store_stock') ?>','Store Stock : Store');">
<i class="fa fa-barcode"></i> Store Stock
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/main_store') ?>','Store Main : Store');">
<i class="fa fa-desktop"></i> Store Main
</a>
</div>
</div>
<div class="module-hero">
<h4>Hospital Stock Control Center</h4>
<p>Track indents, monitor stock movement, and jump to master controls from one screen.</p>
@ -67,14 +87,7 @@ $stockValue = (float) ($stats['stock_value'] ?? 0);
</a>
</div>
<div class="box" style="margin-top:1rem;">
<div class="box-header">
<h3 class="box-title">Workspace</h3>
</div>
<div class="box-body">
<div id="maindiv" class="row"></div>
</div>
</div>
<div class="insight-grid">
<div class="box">
@ -147,5 +160,6 @@ $stockValue = (float) ($stats['stock_value'] ?? 0);
</div>
</div>
</div>
</section>
</div>

View File

@ -0,0 +1,131 @@
<div class="module-hero">
<h4>Hospital Stock Control Center</h4>
<p>Track indents, monitor stock movement, and jump to master controls from one screen.</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<span class="label">Indents Today</span>
<span class="value"><?= (int) ($stats['today_indents'] ?? 0) ?></span>
</div>
<div class="stat-card">
<span class="label">Indents This Month</span>
<span class="value"><?= (int) ($stats['month_indents'] ?? 0) ?></span>
</div>
<div class="stat-card">
<span class="label">Low Stock Items</span>
<span class="value"><?= (int) ($stats['low_stock_count'] ?? 0) ?></span>
</div>
<div class="stat-card">
<span class="label">Near Expiry (60 Days)</span>
<span class="value"><?= (int) ($stats['near_expiry_count'] ?? 0) ?></span>
</div>
</div>
<div class="stats-grid" style="grid-template-columns: repeat(2, minmax(180px, 1fr));">
<div class="stat-card">
<span class="label">Pending Purchase Invoices</span>
<span class="value"><?= (int) ($stats['pending_purchase'] ?? 0) ?></span>
</div>
<div class="stat-card">
<span class="label">Approx. Current Stock Value</span>
<span class="value">Rs. <?= number_format($stockValue, 2) ?></span>
</div>
</div>
<div class="action-grid">
<a class="action-card" href="javascript:load_form_div('/Storestock/Indent_List','maindiv','Store : Indent');">
<span class="icon"><i class="fa fa-shopping-cart"></i></span>
<h5>Indent Desk</h5>
<p>Create and process departmental item requests.</p>
</a>
<a class="action-card" href="javascript:load_form_div('/Storestock/Report_2','maindiv','Day Report :Store');">
<span class="icon"><i class="fa fa-line-chart"></i></span>
<h5>Day Report</h5>
<p>Review daily issue trend and transaction summary.</p>
</a>
<a class="action-card" href="javascript:load_form_div('/Storestock/store_stock','maindiv','Store Stock : Store');">
<span class="icon"><i class="fa fa-barcode"></i></span>
<h5>Stock Search</h5>
<p>Find real-time availability with reorder controls.</p>
</a>
<a class="action-card" href="javascript:load_form_div('/Storestock/main_store','maindiv','Store Main : Store');">
<span class="icon"><i class="fa fa-cogs"></i></span>
<h5>Store Controls</h5>
<p>Open masters, inventory, and procurement tools.</p>
</a>
</div>
<div class="insight-grid">
<div class="box">
<div class="box-header">
<h3 class="box-title">Low Stock Alerts</h3>
</div>
<div class="box-body">
<div class="table-wrap">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Item</th>
<th style="width:120px;">Current Unit</th>
<th style="width:140px;">Reorder Threshold</th>
</tr>
</thead>
<tbody>
<?php $lowStockItems = $lists['low_stock_items'] ?? []; ?>
<?php if (empty($lowStockItems)) { ?>
<tr><td colspan="3" class="text-center text-muted">No low-stock items right now.</td></tr>
<?php } ?>
<?php foreach ($lowStockItems as $row) { ?>
<tr>
<td><?= esc($row['item_name'] ?? '') ?></td>
<td><?= esc($row['cur_units'] ?? 0) ?></td>
<td><?= esc($row['re_order_qty'] ?? 0) ?> x <?= esc($row['packing'] ?? 1) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
<h3 class="box-title">Near Expiry Alerts</h3>
</div>
<div class="box-body">
<div class="table-wrap">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Item / Batch</th>
<th style="width:100px;">Expiry</th>
<th style="width:90px;">Days Left</th>
<th style="width:95px;">Cur Packs</th>
</tr>
</thead>
<tbody>
<?php $nearExpiryItems = $lists['near_expiry_items'] ?? []; ?>
<?php if (empty($nearExpiryItems)) { ?>
<tr><td colspan="4" class="text-center text-muted">No near-expiry batches in next 60 days.</td></tr>
<?php } ?>
<?php foreach ($nearExpiryItems as $row) { ?>
<tr>
<td><?= esc($row['item_name'] ?? '') ?> <span class="text-muted">(<?= esc($row['batch_no'] ?? '-') ?>)</span></td>
<td><?= esc($row['expiry_str'] ?? '-') ?></td>
<td>
<span class="mini-badge <?= ((int) ($row['days_left'] ?? 0)) <= 15 ? 'danger' : 'warn' ?>">
<?= (int) ($row['days_left'] ?? 0) ?>
</span>
</td>
<td><?= esc($row['cur_packs'] ?? 0) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>

View File

@ -1,40 +1,66 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<div class="col-md-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Day Report - Store</h3>
<section class="content">
<div class="module-shell-head">
<div class="module-shell-title">
<span class="main">Store Stock</span>
<span class="sub">Day Report</span>
</div>
<div class="box-body">
<form class="form-day-report" method="post">
<?= csrf_field() ?>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>From Date</label>
<input type="date" class="form-control input-sm" id="date_from" name="date_from"
value="<?= date('Y-m-d') ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>To Date</label>
<input type="date" class="form-control input-sm" id="date_to" name="date_to"
value="<?= date('Y-m-d') ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group" style="margin-top:24px;">
<button type="submit" class="btn btn-info btn-sm">Generate Report</button>
</div>
</div>
</div>
</form>
<div id="day_report_result" class="row"></div>
<div class="module-nav-tabs">
<a class="tab" href="javascript:load_form('<?= base_url('Storestock') ?>','Hospital Stock');">
<i class="fa fa-home"></i> Dashboard
</a>
<a class="tab " href="javascript:load_form('<?= base_url('Storestock/Indent_List') ?>','Store : Indent');">
<i class="fa fa-shopping-cart"></i> Indent
</a>
<a class="tab active" href="javascript:load_form('<?= base_url('Storestock/Report_2') ?>','Day Report :Store');">
<i class="fa fa-line-chart"></i> Day Report
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/store_stock') ?>','Store Stock : Store');">
<i class="fa fa-barcode"></i> Store Stock
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/main_store') ?>','Store Main : Store');">
<i class="fa fa-desktop"></i> Store Main
</a>
</div>
</div>
<div class="workspace-shell">
<div class="col-md-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Day Report - Store</h3>
</div>
<div class="box-body">
<form class="form-day-report" method="post">
<?= csrf_field() ?>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>From Date</label>
<input type="date" class="form-control input-sm" id="date_from" name="date_from"
value="<?= date('Y-m-d') ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>To Date</label>
<input type="date" class="form-control input-sm" id="date_to" name="date_to"
value="<?= date('Y-m-d') ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group" style="margin-top:24px;">
<button type="submit" class="btn btn-info btn-sm">Generate Report</button>
</div>
</div>
</div>
</form>
<div id="day_report_result" class="row"></div>
</div>
</div>
</div>
</section>
</div>
<script>
$(document).ready(function () {
$('form.form-day-report').on('submit', function (e) {

View File

@ -1,45 +1,79 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<div class="col-md-12">
<div class="box">
<div class="box-header">
<div class="toolbar-row">
<div class="left">
<h3 class="box-title" style="margin-right:.65rem;">Indent List</h3>
<button type="button" class="pill-btn active">Live Queue</button>
<button type="button" class="pill-btn" onclick="$('#indent-grid').DataTable().ajax.reload();">Refresh</button>
</div>
<div class="right">
<a class="btn btn-primary btn-sm" href="javascript:load_form_div('/Storestock/new_indent','maindiv');">
<i class="fa fa-plus"></i> New Indent
</a>
</div>
</div>
<section class="content">
<div class="module-shell-head">
<div class="module-shell-title">
<span class="main">Store Stock</span>
<span class="sub">Indent</span>
</div>
<div class="box-body">
<div class="table-wrap">
<table class="table table-bordered table-striped" id="indent-grid" width="100%">
<thead>
<tr>
<th>Indent No.</th>
<th>Department</th>
<th>Date</th>
<th style="display:none;">ID</th>
</tr>
</thead>
<thead>
<tr>
<td><input type="text" data-column="0" class="form-control input-sm" placeholder="Indent No."></td>
<td><input type="text" data-column="1" class="form-control input-sm" placeholder="Department"></td>
<td><input type="date" data-column="2" class="form-control input-sm"></td>
<td style="display:none;"></td>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="module-nav-tabs">
<a class="tab" href="javascript:load_form('<?= base_url('Storestock') ?>','Hospital Stock');">
<i class="fa fa-home"></i> Dashboard
</a>
<a class="tab active" href="javascript:load_form('<?= base_url('Storestock/Indent_List') ?>','Store : Indent');">
<i class="fa fa-shopping-cart"></i> Indent
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/Report_2') ?>','Day Report :Store');">
<i class="fa fa-line-chart"></i> Day Report
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/store_stock') ?>','Store Stock : Store');">
<i class="fa fa-barcode"></i> Store Stock
</a>
<a class="tab" href="javascript:load_form('<?= base_url('Storestock/main_store') ?>','Store Main : Store');">
<i class="fa fa-desktop"></i> Store Main
</a>
</div>
</div>
<div class="workspace-shell">
<div class="box">
<div class="box-header">
<h3 class="box-title">Workspace</h3>
</div>
<div class="box-body" id="maindiv">
<div class="box" style="margin-bottom:0;">
<div class="box-header">
<div class="toolbar-row">
<div class="left">
<h3 class="box-title" style="margin-right:.65rem;">Indent List</h3>
<button type="button" class="pill-btn active">Live Queue</button>
<button type="button" class="pill-btn" onclick="$('#indent-grid').DataTable().ajax.reload();">Refresh</button>
</div>
<div class="right">
<a class="btn btn-primary btn-sm" href="javascript:load_form_div('/Storestock/new_indent','maindiv');">
<i class="fa fa-plus"></i> New Indent
</a>
</div>
</div>
</div>
<div class="box-body">
<div class="table-wrap">
<table class="table table-bordered table-striped" id="indent-grid" width="100%">
<thead>
<tr>
<th>Indent No.</th>
<th>Department</th>
<th>Date</th>
<th style="display:none;">ID</th>
</tr>
</thead>
<thead>
<tr>
<td><input type="text" data-column="0" class="form-control input-sm" placeholder="Indent No."></td>
<td><input type="text" data-column="1" class="form-control input-sm" placeholder="Department"></td>
<td><input type="date" data-column="2" class="form-control input-sm"></td>
<td style="display:none;"></td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<script type="text/javascript">
@ -50,7 +84,7 @@ $(document).ready(function () {
"processing": true,
"serverSide": true,
"ajax": {
url: "Storestock/getIndentTable",
url: "<?= base_url('Storestock/getIndentTable') ?>",
dataType: "json",
type: "post",
data: function (d) {

View File

@ -0,0 +1,97 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<section class="content-header">
<h1>
Purchase
<small>Store</small>
</h1>
</section>
<section class="content">
<div class="module-hero">
<h4>Store Purchase Control Center</h4>
<p>Track supplier invoices, open edits in a persistent sub panel, and keep the invoice list visible while you work.</p>
</div>
<div class="box">
<div class="box-header d-flex justify-content-between align-items-center">
<h3 class="box-title mb-0">Purchase Invoices</h3>
<button type="button" class="btn btn-warning btn-sm" id="btnNewPurchase">New Purchase / Challan Invoice</button>
</div>
<div class="box-body">
<form id="purchase-search-form" class="row g-2 align-items-center" method="post" action="javascript:void(0)">
<?= csrf_field() ?>
<div class="col-md-6">
<input class="form-control" type="text" id="txtsearch" name="txtsearch" placeholder="Search by invoice no. or supplier">
</div>
<div class="col-auto">
<button type="submit" class="btn btn-info">Search Purchase Invoice</button>
</div>
<div class="col-auto">
<button type="button" id="btnResetPurchase" class="btn btn-outline-secondary">Reset</button>
</div>
</form>
</div>
</div>
<div class="box">
<div class="box-header">
<h3 class="box-title">Recent Purchase Invoices</h3>
</div>
<div class="box-body" id="purchase-main-panel">
<?= $this->include('storestock/purchase_supp_list') ?>
</div>
</div>
<div id="purchase-sub-main" style="display:none;" class="mt-3">
<div id="searchresult"></div>
</div>
</section>
</div>
<script>
(function () {
window.openStorestockPurchaseSubView = function (url, title) {
var $root = $('#purchase-sub-main');
var $listPanel = $('#purchase-main-panel');
if ($root.length) {
$listPanel.hide();
$root.show();
load_form_div(url, 'searchresult', title || 'Purchase : Store');
return;
}
load_form_div(url, 'maindiv', title || 'Purchase : Store');
};
window.closeStorestockPurchaseSubView = function () {
var $root = $('#purchase-sub-main');
var $listPanel = $('#purchase-main-panel');
if ($root.length) {
$('#searchresult').empty();
$root.hide();
$listPanel.show();
}
};
$('#btnNewPurchase').off('click').on('click', function () {
openStorestockPurchaseSubView('<?= base_url('Storestock/PurchaseNew') ?>', 'New Purchase / Challan Invoice : Store');
});
$('#btnResetPurchase').off('click').on('click', function () {
$('#txtsearch').val('');
$('#purchase-search-form').trigger('submit');
});
$('#purchase-search-form').off('submit').on('submit', function (event) {
event.preventDefault();
$.post('<?= base_url('Storestock/PurchaseInvoice') ?>', $(this).serialize(), function (html) {
$('#purchase-main-panel').html(html || '');
}).fail(function () {
$('#purchase-main-panel').html('<div class="alert alert-danger mb-0">Failed to load purchase list.</div>');
});
});
})();
</script>

View File

@ -0,0 +1,136 @@
<?php $invoice = $inv_master_data[0] ?? null; ?>
<?php if (! $invoice): ?>
<div class="alert alert-warning mb-0">Purchase invoice not found.</div>
<?php else: ?>
<div class="box">
<div class="box-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<h3 class="box-title mb-0">Purchase Invoice / Supplier : <?= esc((string) ($invoice->name_supplier ?? '-')) ?></h3>
<small class="text-muted">
Invoice No.: <?= esc((string) ($invoice->Invoice_no ?? '')) ?> |
Invoice Date: <?= esc((string) ($invoice->str_date_of_invoice ?? '')) ?>
</small>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="closeStorestockPurchaseSubView();">Back to List</button>
<button type="button" class="btn btn-warning" onclick="openStorestockPurchaseSubView('<?= base_url('Storestock/PurchaseMasterEdit/' . (int) ($invoice->id ?? 0)) ?>','Purchase : Edit');">Reload Invoice</button>
<a href="<?= base_url('Storestock/print_purchase/' . (int) ($invoice->id ?? 0)) ?>" target="_blank" class="btn btn-secondary"><i class="fa fa-print"></i> Print</a>
</div>
</div>
<div class="box-body">
<form id="storestock-purchase-edit-form" class="row g-3" method="post" action="javascript:void(0)">
<?= csrf_field() ?>
<input type="hidden" id="hid_purchaseid" name="hid_purchaseid" value="<?= (int) ($invoice->id ?? 0) ?>">
<div class="col-md-4">
<label class="form-label">Supplier</label>
<select class="form-select" id="input_supplier" name="input_supplier" <?= ((int) ($invoice->inv_status ?? 0)) === 0 ? '' : 'disabled' ?>>
<?php foreach (($supplier_data ?? []) as $row): ?>
<option value="<?= (int) ($row->sid ?? 0) ?>" <?= ((int) ($row->sid ?? 0)) === (int) ($invoice->sid ?? 0) ? 'selected' : '' ?>><?= esc($row->name_supplier ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Bill Type</label>
<select class="form-select" name="cbo_billtype" id="cbo_billtype" <?= ((int) ($invoice->inv_status ?? 0)) === 0 ? '' : 'disabled' ?>>
<option value="0" <?= ((int) ($invoice->ischallan ?? 0)) === 0 ? 'selected' : '' ?>>Invoice</option>
<option value="1" <?= ((int) ($invoice->ischallan ?? 0)) === 1 ? 'selected' : '' ?>>Challan</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Invoice No.</label>
<input class="form-control" name="input_invoicecode" type="text" value="<?= esc((string) ($invoice->Invoice_no ?? '')) ?>" <?= ((int) ($invoice->inv_status ?? 0)) === 0 ? '' : 'readonly' ?>>
</div>
<div class="col-md-2">
<label class="form-label">Date of Invoice</label>
<input class="form-control" name="datepicker_invoice" type="text" value="<?= esc((string) ($invoice->str_date_of_invoice ?? '')) ?>" <?= ((int) ($invoice->inv_status ?? 0)) === 0 ? '' : 'readonly' ?>>
</div>
<div class="col-md-2 d-flex align-items-end gap-2">
<button type="button" class="btn btn-primary" id="btn_update_purchase" <?= ((int) ($invoice->inv_status ?? 0)) === 0 ? '' : 'disabled' ?>>Update</button>
<button type="button" class="btn btn-outline-secondary" id="btn_toggle_status">Toggle Status</button>
</div>
</form>
<div class="table-responsive mt-4">
<table class="table table-bordered table-striped table-sm align-middle">
<thead>
<tr>
<th>#</th>
<th>Item Name</th>
<th>Batch No</th>
<th>Exp.</th>
<th>MRP</th>
<th>Qty.</th>
<th>Rate</th>
<th>Amount</th>
<th>Disc.</th>
<th>Tax Amount</th>
<th>CGST</th>
<th>SGST</th>
<th>Net Amount</th>
</tr>
</thead>
<tbody>
<?php if (empty($purchase_item)): ?>
<tr><td colspan="13" class="text-center text-muted">No items added yet.</td></tr>
<?php endif; ?>
<?php $srno = 0; foreach (($purchase_item ?? []) as $row): $srno++; ?>
<tr>
<td><?= $srno ?></td>
<td><?= esc((string) ($row->Item_name ?? $row->item_name ?? '')) ?></td>
<td><?= esc((string) ($row->batch_no ?? '-')) ?></td>
<td><?= esc((string) ($row->expiry_date ?? '-')) ?></td>
<td><?= esc((string) ($row->mrp ?? '0')) ?></td>
<td><?= esc((string) ((float) ($row->qty ?? 0))) ?><?= ((float) ($row->qty_free ?? 0)) > 0 ? ' + ' . esc((string) ((float) ($row->qty_free ?? 0))) : '' ?></td>
<td><?= esc((string) ($row->purchase_price ?? '0')) ?></td>
<td><?= esc((string) ($row->amount ?? '0')) ?></td>
<td><?= esc((string) ($row->discount ?? $row->disc_amount ?? '0')) ?></td>
<td><?= esc((string) ($row->taxable_amount ?? '0')) ?></td>
<td><?= esc((string) ($row->CGST_per ?? '0')) ?></td>
<td><?= esc((string) ($row->SGST_per ?? '0')) ?></td>
<td><?= esc((string) ($row->net_amount ?? '0')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr>
<th colspan="9" class="text-end">Totals</th>
<th><?= esc((string) ($invoice->Taxable_Amt ?? '0')) ?></th>
<th><?= esc((string) ($invoice->CGST_Amt ?? '0')) ?></th>
<th><?= esc((string) ($invoice->SGST_Amt ?? '0')) ?></th>
<th><?= esc((string) ($invoice->T_Net_Amount ?? '0')) ?></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<script>
(function () {
$('#btn_update_purchase').off('click').on('click', function () {
$.post('<?= base_url('Storestock/UpdatePurchase') ?>', $('#storestock-purchase-edit-form').serialize(), function (data) {
if (!data || typeof data !== 'object') {
notify('error', 'Please Attention', 'Unexpected response');
return;
}
if ((data.insertid || 0) === 0) {
notify('error', 'Please Attention', data.show_text || 'Please check input');
return;
}
notify('success', 'Update Success', data.show_text || 'Data Update');
openStorestockPurchaseSubView('<?= base_url('Storestock/PurchaseMasterEdit/' . (int) ($invoice->id ?? 0)) ?>', 'Purchase : Edit');
}, 'json').fail(function () {
notify('error', 'Please Attention', 'Unable to update invoice');
});
});
$('#btn_toggle_status').off('click').on('click', function () {
var nextStatus = <?= ((int) ($invoice->inv_status ?? 0)) === 0 ? 1 : 0 ?>;
openStorestockPurchaseSubView('<?= base_url('Storestock/UpdatePurchaseInvoiceStatus/' . (int) ($invoice->id ?? 0)) ?>/' + nextStatus, 'Purchase : Edit');
});
})();
</script>
<?php endif; ?>

View File

@ -0,0 +1,60 @@
<div class="box">
<div class="box-header d-flex justify-content-between align-items-center">
<h3 class="box-title mb-0">New Purchase Invoice</h3>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="closeStorestockPurchaseSubView();">Back to List</button>
</div>
<div class="box-body">
<form id="storestock-purchase-new-form" class="row g-3" method="post" action="javascript:void(0)">
<?= csrf_field() ?>
<div class="col-md-4">
<label class="form-label">Supplier</label>
<select class="form-select" id="input_supplier" name="input_supplier">
<?php foreach (($supplier_data ?? []) as $row): ?>
<option value="<?= (int) ($row->sid ?? 0) ?>"><?= esc($row->name_supplier ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label class="form-label">Bill Type</label>
<select class="form-select" name="cbo_billtype" id="cbo_billtype">
<option value="0">Invoice</option>
<option value="1">Challan</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Invoice / Challan No.</label>
<input class="form-control" name="input_invoicecode" placeholder="Invoice No." type="text">
</div>
<div class="col-md-3">
<label class="form-label">Date of Invoice</label>
<input class="form-control" name="datepicker_invoice" id="datepicker_invoice" type="text" value="<?= date('d/m/Y') ?>">
</div>
<div class="col-12">
<button type="button" class="btn btn-primary" id="btn_create_purchase">Create</button>
</div>
</form>
</div>
</div>
<script>
(function () {
$('#btn_create_purchase').off('click').on('click', function () {
$.post('<?= base_url('Storestock/CreatePurchase') ?>', $('#storestock-purchase-new-form').serialize(), function (data) {
if (!data || typeof data !== 'object') {
notify('error', 'Please Attention', 'Unexpected response');
return;
}
if ((data.insertid || 0) <= 0) {
notify('error', 'Please Attention', data.show_text || 'Unable to create purchase invoice');
return;
}
notify('success', 'Please Attention', 'Invoice Added : ID->' + data.insertid);
openStorestockPurchaseSubView('<?= base_url('Storestock/PurchaseMasterEdit') ?>/' + data.insertid, 'Purchase : Edit');
}, 'json').fail(function () {
notify('error', 'Please Attention', 'Unable to create purchase invoice');
});
});
})();
</script>

View File

@ -0,0 +1,51 @@
<?php $invoice = $purchase_invoice ?? null; ?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Purchase Invoice</title>
<style>
body { font-family: Arial, Helvetica, sans-serif; margin: 18px; color: #1f2937; }
.title { font-size: 22px; font-weight: 700; color: #0f2f67; }
.sub { margin-top: 4px; color: #6b7280; font-size: 13px; }
table { width: 100%; border-collapse: collapse; margin-top: 14px; font-size: 13px; }
th, td { border: 1px solid #d1d5db; padding: 6px 8px; text-align: left; }
th { background: #f3f4f6; }
.right { text-align: right; }
</style>
</head>
<body>
<div class="title">Purchase Invoice</div>
<div class="sub">Supplier: <?= esc((string) ($invoice->name_supplier ?? '-')) ?> | Invoice No.: <?= esc((string) ($invoice->Invoice_no ?? '')) ?> | Date: <?= esc((string) ($invoice->str_date_of_invoice ?? '')) ?></div>
<table>
<thead>
<tr>
<th>#</th>
<th>Item</th>
<th>Batch</th>
<th>Exp.</th>
<th class="right">Qty</th>
<th class="right">Rate</th>
<th class="right">Amount</th>
<th class="right">Net</th>
</tr>
</thead>
<tbody>
<?php $srno = 0; foreach (($purchase_invoice_item ?? []) as $row): $srno++; ?>
<tr>
<td><?= $srno ?></td>
<td><?= esc((string) ($row->Item_name ?? $row->item_name ?? '')) ?></td>
<td><?= esc((string) ($row->batch_no ?? '-')) ?></td>
<td><?= esc((string) ($row->expiry_date ?? '-')) ?></td>
<td class="right"><?= esc((string) ((float) ($row->qty ?? 0))) ?></td>
<td class="right"><?= esc((string) ($row->purchase_price ?? '0')) ?></td>
<td class="right"><?= esc((string) ($row->amount ?? '0')) ?></td>
<td class="right"><?= esc((string) ($row->net_amount ?? '0')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,24 @@
<?php $invoice = $purchase_return_invoice ?? null; ?>
<?php if (! $invoice): ?>
<div class="alert alert-warning mb-0">Purchase return invoice not found.</div>
<?php else: ?>
<div class="box">
<div class="box-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<div>
<h3 class="box-title mb-0">Purchase Return Invoice / Supplier : <?= esc((string) ($invoice->name_supplier ?? '-')) ?></h3>
<small class="text-muted">
Invoice No.: <?= esc((string) ($invoice->Invoice_no ?? '')) ?> |
Invoice Date: <?= esc((string) ($invoice->str_date_of_invoice ?? '')) ?>
</small>
</div>
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-secondary" onclick="load_form_div('<?= base_url('Storestock/Purchase_return') ?>','searchresult','Purchase Return');">Back</button>
<button type="button" class="btn btn-warning" onclick="load_form_div('<?= base_url('Storestock/PurchaseReturnInvoiceEdit/' . (int) ($invoice->id ?? 0)) ?>','searchresult','Purchase Return Invoice Edit');">Reload Invoice</button>
<a href="<?= base_url('Storestock/print_purchase_return/' . (int) ($invoice->id ?? 0)) ?>" target="_blank" class="btn btn-secondary"><i class="fa fa-print"></i> Print</a>
</div>
</div>
<div class="box-body pt-3">
<?= $content ?? '' ?>
</div>
</div>
<?php endif; ?>

View File

@ -0,0 +1,28 @@
<?php $rows = $purchase_return_invoice_item ?? []; ?>
<div class="table-responsive">
<table id="purchase_return_report_list" class="table table-bordered table-striped table-sm align-middle">
<thead>
<tr>
<th>#</th>
<th>Item</th>
<th>Batch</th>
<th>Expiry</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="5" class="text-center text-muted">No return items found.</td></tr>
<?php endif; ?>
<?php $srno = 0; foreach ($rows as $row): $srno++; ?>
<tr>
<td><?= $srno ?></td>
<td><?= esc((string) ($row->Item_name ?? '')) ?></td>
<td><?= esc((string) ($row->batch_no_r ?? $row->batch_no ?? '-')) ?></td>
<td><?= esc((string) ($row->expiry_date_r ?? $row->expiry_date ?? '-')) ?></td>
<td><?= esc((string) ($row->r_qty ?? $row->qty ?? '0')) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>

View File

@ -0,0 +1,50 @@
<div class="box">
<div class="box-header d-flex justify-content-between align-items-center">
<h3 class="box-title mb-0">New Purchase Return Invoice</h3>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="load_form_div('<?= base_url('Storestock/Purchase_return') ?>','searchresult','Purchase Return');">Back</button>
</div>
<div class="box-body pt-3">
<form id="new-purchase-return-form" class="row g-2" method="post" action="javascript:void(0)">
<?= csrf_field() ?>
<div class="col-md-4">
<label class="form-label">Supplier</label>
<select class="form-select" id="input_supplier" name="input_supplier">
<?php foreach (($supplier_data ?? []) as $row): ?>
<option value="<?= (int) ($row->sid ?? 0) ?>"><?= esc($row->name_supplier ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Date of Invoice</label>
<input class="form-control" name="datepicker_invoice" id="datepicker_invoice" type="text" value="<?= date('d/m/Y') ?>">
</div>
<div class="col-12">
<button type="button" class="btn btn-primary" id="btn_create_purchase_return" accesskey="C">Create</button>
</div>
</form>
</div>
</div>
<script>
(function () {
$('#btn_create_purchase_return').off('click').on('click', function () {
$.post('<?= base_url('Storestock/create_purchase_return') ?>', $('#new-purchase-return-form').serialize(), function (data) {
if (!data || typeof data !== 'object') {
notify('error', 'Please Attention', 'Unexpected response');
return;
}
if ((data.insertid || 0) <= 0) {
notify('error', 'Please Attention', data.show_text || 'Unable to create purchase return invoice');
return;
}
notify('success', 'Please Attention', 'Return Invoice : ID->' + data.insertid);
load_form_div('<?= base_url('Storestock/PurchaseReturnInvoiceEdit') ?>/' + data.insertid, 'searchresult', 'Purchase Return Invoice Edit');
}, 'json').fail(function () {
notify('error', 'Please Attention', 'Unable to create purchase return invoice');
});
});
})();
</script>

View File

@ -0,0 +1,51 @@
<?php $rows = $purchase_list ?? []; ?>
<div class="table-responsive">
<table id="purchase-report-list" class="table table-bordered table-striped table-sm align-middle">
<thead>
<tr>
<th>Invoice ID</th>
<th>Supplier</th>
<th>Date</th>
<th>Amount</th>
<th style="width:220px;">Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr>
<td class="text-center text-muted">No purchase invoices found.</td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<?php endif; ?>
<?php foreach ($rows as $row): ?>
<tr class="<?= ((int) ($row->ischallan ?? 0)) === 1 ? 'text-danger' : '' ?>">
<td><?= esc((string) ($row->Invoice_no ?? '')) ?></td>
<td><?= esc((string) ($row->name_supplier ?? '')) ?></td>
<td><?= esc((string) ($row->str_date_of_invoice ?? $row->date_of_invoice ?? '')) ?></td>
<td><?= esc((string) number_format((float) ($row->tamount ?? 0), 2)) ?></td>
<td>
<button type="button" class="btn btn-warning btn-sm" onclick="openStorestockPurchaseSubView('<?= base_url('Storestock/PurchaseMasterEdit/' . (int) ($row->id ?? 0)) ?>','Purchase : Edit');">View &amp; Edit</button>
<a href="<?= base_url('Storestock/print_purchase/' . (int) ($row->id ?? 0)) ?>" target="_blank" class="btn btn-secondary btn-sm"><i class="fa fa-print"></i> Print</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<script>
(function () {
if (window.jQuery && $.fn.DataTable) {
if ($.fn.DataTable.isDataTable('#purchase-report-list')) {
$('#purchase-report-list').DataTable().destroy();
}
$('#purchase-report-list').DataTable({
pageLength: 25,
order: [[0, 'desc']]
});
}
})();
</script>