diff --git a/app/Exceptions/InceptNebLimitException.php b/app/Exceptions/InceptNebLimitException.php new file mode 100644 index 00000000..bc53ce38 --- /dev/null +++ b/app/Exceptions/InceptNebLimitException.php @@ -0,0 +1,7 @@ +session()->put('formdata', $formdata); if(isset($formdata) && !empty($formdata) && $formdata->user_id != '') { - $post = $this->processformdata($formdata->form_id, $post, $formdata->user_id, $formdata->partition); - Log::debug(print_r($post, true)); - $finfo = LambdaRequest::postrequest('form', 'formentity/submitform', ['formdata' => $post, 'entity_id' => $formdata->form_id, 'user_id' => $formdata->user_id, 'partition' => $formdata->partition ]); - + $inceptUsageId = null; + $inceptAllocation = null; + if (($formdata->form_id ?? '') === InceptNebAllocation::FORM_ID) { + $inceptAllocation = new InceptNebAllocation(); + try { + $inceptUsageId = $inceptAllocation->reserveFromSubmission($post, $formdata); + } catch (InceptNebLimitException $e) { + return response()->json(['status' => 'failure', 'message' => $e->getMessage()]); + } catch (\Throwable $e) { + Log::error('Incept nebulizer allocation check failed: ' . $e->getMessage()); + return response()->json(['status' => 'failure', 'message' => 'Unable to check nebulizer allocation. Please try again.']); + } + } + try { + $post = $this->processformdata($formdata->form_id, $post, $formdata->user_id, $formdata->partition); + Log::debug(print_r($post, true)); + $finfo = LambdaRequest::postrequest('form', 'formentity/submitform', ['formdata' => $post, 'entity_id' => $formdata->form_id, 'user_id' => $formdata->user_id, 'partition' => $formdata->partition ]); + } catch (\Throwable $e) { + if ($inceptAllocation && $inceptUsageId) { + $inceptAllocation->release($inceptUsageId); + Log::error('Form submit failed after nebulizer reservation: ' . $e->getMessage()); + return response()->json(['status' => 'failure', 'message' => 'Something went wrong, please try again after some time!']); + } + throw $e; + } + Log::debug(print_r($finfo, true)); if($finfo->status == 'success') { + if ($inceptAllocation && $inceptUsageId) { + $submissionId = $finfo->data->submission->id ?? ''; + $inceptAllocation->attachSubmission($inceptUsageId, $submissionId); + } return response()->json(['status' => 'success']); } else { + if ($inceptAllocation && $inceptUsageId) { + $inceptAllocation->release($inceptUsageId); + } if($finfo->statusCode == 401) { return response()->json(['status' => 'failure', 'message' => $finfo->data ]); } return response()->json(['status' => 'failure']); - } + } } else { return response()->json(['status' => 'failure']); } @@ -8296,12 +8327,51 @@ class FormController extends Controller return []; break; + case InceptNebAllocation::FORM_ID: + $service = new InceptNebAllocation(); + $empId = ''; + if (isset($formdata->user_info)) { + foreach (['employee_code', 'emp_employee_code', 'emp_id'] as $key) { + if (!empty($formdata->user_info->{$key})) { + $empId = $formdata->user_info->{$key}; + break; + } + } + } + if ($empId === '' && !empty($formdata->employee_code)) { + $empId = $formdata->employee_code; + } + if ($empId === '' && !empty($formdata->user_id)) { + $empId = $service->resolveEmpIdFromUser($formdata->user_id); + } + return ['incept_neb' => $service->summaryForEmployee($empId)]; + break; + default: return []; break; } } + public function inceptNebBalance(Request $request) + { + $service = new InceptNebAllocation(); + $empId = trim((string) $request->input('emp_id', '')); + if ($empId === '') { + $empId = $service->resolveEmpIdFromUser($request->input('user_id')); + } + if ($empId === '') { + $formdata = $request->session()->get('formdata'); + $empId = $service->resolveEmpId([], $formdata); + } + + return response()->json([ + 'status' => 'success', + 'allocation' => $service->summaryForEmployee($empId), + 'msl' => $service->mslUsage($request->input('msl_code', '')), + ]); + } + public function getformfielddata(Request $request) { $post = $request->except('_token'); $history = LambdaRequest::postrequest('form', 'formentity/getformhistory', ['form_id' => $post['form_id'], 'user_id' => $post['user_id'], 'partition' => $post['partition'] ]); diff --git a/app/Services/InceptNebAllocation.php b/app/Services/InceptNebAllocation.php new file mode 100644 index 00000000..82e3b5ef --- /dev/null +++ b/app/Services/InceptNebAllocation.php @@ -0,0 +1,269 @@ + ['label' => 'Robust', 'column' => 'robust_qty'], + 'mini' => ['label' => 'Mini', 'column' => 'mini_qty'], + 'control_d' => ['label' => 'Control D', 'column' => 'control_d_qty'], + ]; + + public function summaryForEmployee($empId) + { + $empId = $this->normalizeEmpId($empId); + $allocation = $empId === '' ? null : DB::table('incept_neb_allocations')->where('emp_id', $empId)->first(); + $usedByType = []; + if ($empId !== '') { + $rows = DB::table('incept_neb_usage') + ->select('nebulizer_type', DB::raw('SUM(qty) as used_qty')) + ->where('emp_id', $empId) + ->groupBy('nebulizer_type') + ->get(); + foreach ($rows as $row) { + $usedByType[$row->nebulizer_type] = (int) $row->used_qty; + } + } + + $types = []; + foreach (self::TYPES as $key => $meta) { + $allocated = $allocation ? (int) ($allocation->{$meta['column']} ?? 0) : 0; + $used = (int) ($usedByType[$key] ?? 0); + $types[$key] = [ + 'label' => $meta['label'], + 'allocated' => $allocated, + 'used' => $used, + 'balance' => max(0, $allocated - $used), + ]; + } + + return [ + 'emp_id' => $allocation->emp_id ?? $empId, + 'emp_name' => $allocation->emp_name ?? '', + 'team' => $allocation->team ?? '', + 'designation' => $allocation->designation ?? '', + 'has_allocation' => (bool) $allocation, + 'types' => $types, + ]; + } + + public function mslUsage($mslCode) + { + $mslCode = trim((string) $mslCode); + $used = 0; + if ($mslCode !== '') { + $used = (int) DB::table('incept_neb_usage')->where('msl_code', $mslCode)->sum('qty'); + } + + return [ + 'msl_code' => $mslCode, + 'used' => $used, + 'limit' => self::MSL_LIMIT, + 'remaining' => max(0, self::MSL_LIMIT - $used), + ]; + } + + public function resolveEmpIdFromUser($userId) + { + $userId = trim((string) $userId); + if ($userId === '' || !Schema::hasTable('users')) { + return ''; + } + + $query = DB::table('users'); + $user = (clone $query)->where('user_id', $userId)->first(); + if (!$user) { + $user = DB::table('users')->where('id', $userId)->first(); + } + if (!$user) { + return ''; + } + + foreach (['emp_employee_code', 'employee_code', 'emp_id'] as $column) { + if (isset($user->{$column}) && trim((string) $user->{$column}) !== '') { + return $this->normalizeEmpId($user->{$column}); + } + } + + return ''; + } + + public function resolveEmpId($post, $formdata = null) + { + $candidates = []; + if (is_array($post)) { + foreach (['incept_emp_id', 'emp_id', 'employee_code'] as $key) { + if (!empty($post[$key])) { + $candidates[] = $post[$key]; + } + } + if (!empty($post['user_id'])) { + $candidates[] = $this->resolveEmpIdFromUser($post['user_id']); + } + } + if (is_object($formdata) && isset($formdata->user_info)) { + foreach (['employee_code', 'emp_employee_code', 'emp_id'] as $key) { + if (!empty($formdata->user_info->{$key})) { + $candidates[] = $formdata->user_info->{$key}; + } + } + } + if (is_object($formdata) && !empty($formdata->employee_code)) { + $candidates[] = $formdata->employee_code; + } + if (is_object($formdata) && !empty($formdata->user_id)) { + $candidates[] = $this->resolveEmpIdFromUser($formdata->user_id); + } + + foreach ($candidates as $candidate) { + $empId = $this->normalizeEmpId($candidate); + if ($empId !== '') { + return $empId; + } + } + + return ''; + } + + public function reserveFromSubmission(array $post, $formdata) + { + $type = $this->typeKey($post['type_of_nebulizer'] ?? ''); + $qty = (int) ($post['no_of_nebulizer'] ?? 0); + $mslCode = trim((string) ($post['msl_code'] ?? '')); + $empId = $this->resolveEmpId($post, $formdata); + + if ($type === null) { + throw new InceptNebLimitException('Select a nebulizer type.'); + } + if ($qty < 1) { + throw new InceptNebLimitException('Enter the number of nebulizers.'); + } + if ($mslCode === '') { + throw new InceptNebLimitException('MSL code is required.'); + } + if ($empId === '') { + throw new InceptNebLimitException('Employee allocation could not be matched.'); + } + + $locks = []; + try { + $locks[] = $this->acquireLock('incept_neb_emp_' . $empId); + $locks[] = $this->acquireLock('incept_neb_msl_' . $mslCode); + + return DB::transaction(function () use ($empId, $type, $qty, $mslCode) { + $allocation = DB::table('incept_neb_allocations')->where('emp_id', $empId)->lockForUpdate()->first(); + if (!$allocation) { + throw new InceptNebLimitException('No nebulizer allocation is set for employee ' . $empId . '.'); + } + + $column = self::TYPES[$type]['column']; + $allocated = (int) ($allocation->{$column} ?? 0); + $used = (int) DB::table('incept_neb_usage') + ->where('emp_id', $empId) + ->where('nebulizer_type', $type) + ->lockForUpdate() + ->sum('qty'); + $balance = $allocated - $used; + if ($qty > $balance) { + throw new InceptNebLimitException( + 'Only ' . max(0, $balance) . ' ' . self::TYPES[$type]['label'] . ' nebulizer(s) are left in your allocation.' + ); + } + + $mslUsed = (int) DB::table('incept_neb_usage')->where('msl_code', $mslCode)->lockForUpdate()->sum('qty'); + if (($mslUsed + $qty) > self::MSL_LIMIT) { + $remaining = max(0, self::MSL_LIMIT - $mslUsed); + throw new InceptNebLimitException( + 'MSL ' . $mslCode . ' can take ' . $remaining . ' more nebulizer(s). The overall limit is ' . self::MSL_LIMIT . '.' + ); + } + + return DB::table('incept_neb_usage')->insertGetId([ + 'emp_id' => $empId, + 'msl_code' => $mslCode, + 'nebulizer_type' => $type, + 'qty' => $qty, + 'submission_id' => null, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + }); + } finally { + foreach (array_reverse($locks) as $lock) { + DB::select('SELECT RELEASE_LOCK(?)', [$lock]); + } + } + } + + public function attachSubmission($usageId, $submissionId) + { + if (!$usageId) { + return; + } + DB::table('incept_neb_usage')->where('id', $usageId)->update([ + 'submission_id' => (string) $submissionId, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + + public function release($usageId) + { + if (!$usageId) { + return; + } + DB::table('incept_neb_usage')->where('id', $usageId)->delete(); + } + + public function typeKey($label) + { + $value = strtolower(trim((string) $label)); + $value = str_replace([' ', '-'], '_', $value); + if ($value === 'robust') { + return 'robust'; + } + if ($value === 'mini') { + return 'mini'; + } + if ($value === 'control_d' || $value === 'controld') { + return 'control_d'; + } + + return null; + } + + public function normalizeEmpId($value) + { + if (is_int($value) || is_float($value)) { + if ((float) $value === floor((float) $value)) { + return (string) (int) $value; + } + } + $value = trim((string) $value); + $value = str_replace("\xc2\xa0", ' ', $value); + $value = trim($value); + if ($value !== '' && is_numeric($value) && strpos($value, '.') !== false) { + $value = (string) (int) $value; + } + + return $value; + } + + private function acquireLock($name) + { + $name = substr($name, 0, 64); + $row = DB::selectOne('SELECT GET_LOCK(?, 10) AS locked', [$name]); + if (!$row || (int) $row->locked !== 1) { + throw new InceptNebLimitException('Nebulizer allocation is busy. Please try again.'); + } + + return $name; + } +} diff --git a/database/migrations/2026_09_29_000001_create_incept_neb_tables.php b/database/migrations/2026_09_29_000001_create_incept_neb_tables.php new file mode 100644 index 00000000..399dc2c8 --- /dev/null +++ b/database/migrations/2026_09_29_000001_create_incept_neb_tables.php @@ -0,0 +1,44 @@ +bigIncrements('id'); + $table->string('emp_id', 64)->unique(); + $table->string('emp_name')->default(''); + $table->string('team')->default(''); + $table->string('designation')->default(''); + $table->unsignedInteger('robust_qty')->default(0); + $table->unsignedInteger('mini_qty')->default(0); + $table->unsignedInteger('control_d_qty')->default(0); + $table->timestamps(); + }); + } + + if (!Schema::hasTable('incept_neb_usage')) { + Schema::create('incept_neb_usage', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('emp_id', 64)->index(); + $table->string('msl_code', 64)->index(); + $table->string('nebulizer_type', 32)->index(); + $table->unsignedInteger('qty'); + $table->string('submission_id', 128)->nullable()->index(); + $table->timestamps(); + $table->index(['emp_id', 'nebulizer_type'], 'incept_neb_usage_emp_type'); + }); + } + } + + public function down() + { + Schema::dropIfExists('incept_neb_usage'); + Schema::dropIfExists('incept_neb_allocations'); + } +} diff --git a/database/seeders/InceptNebAllocationSeeder.php b/database/seeders/InceptNebAllocationSeeder.php new file mode 100644 index 00000000..0da1bdb6 --- /dev/null +++ b/database/seeders/InceptNebAllocationSeeder.php @@ -0,0 +1,62 @@ +getActiveSheet(); + $rows = $sheet->toArray(null, true, true, true); + $service = new InceptNebAllocation(); + $now = date('Y-m-d H:i:s'); + $count = 0; + + foreach ($rows as $index => $row) { + if ($index === 1) { + continue; + } + $empId = $service->normalizeEmpId($row['A'] ?? ''); + if ($empId === '' || strcasecmp($empId, 'EMP ID') === 0) { + continue; + } + + DB::table('incept_neb_allocations')->updateOrInsert( + ['emp_id' => $empId], + [ + 'emp_name' => $this->clean($row['B'] ?? ''), + 'team' => $this->clean($row['C'] ?? ''), + 'designation' => $this->clean($row['D'] ?? ''), + 'robust_qty' => max(0, (int) ($row['E'] ?? 0)), + 'mini_qty' => max(0, (int) ($row['F'] ?? 0)), + 'control_d_qty' => max(0, (int) ($row['G'] ?? 0)), + 'updated_at' => $now, + 'created_at' => $now, + ] + ); + $count++; + } + + if (isset($this->command)) { + $this->command->info('Imported ' . $count . ' nebulizer allocations.'); + } + } + + private function clean($value) + { + $value = str_replace("\xc2\xa0", ' ', (string) $value); + $value = preg_replace('/\s+/', ' ', $value); + + return trim($value); + } +} diff --git a/resources/views/form/rapport-team3/1786346682-7ed41cf4-f562-4eb7-8d53-efedd0133f41.blade.php b/resources/views/form/rapport-team3/1786346682-7ed41cf4-f562-4eb7-8d53-efedd0133f41.blade.php index 0dd4ec7b..c00c43fa 100644 --- a/resources/views/form/rapport-team3/1786346682-7ed41cf4-f562-4eb7-8d53-efedd0133f41.blade.php +++ b/resources/views/form/rapport-team3/1786346682-7ed41cf4-f562-4eb7-8d53-efedd0133f41.blade.php @@ -39,9 +39,64 @@
| + @foreach(['robust', 'mini', 'control_d'] as $typeKey) + | {{ $inceptTypes[$typeKey]['label'] ?? $typeKey }} | + @endforeach +
|---|---|
| Allocation | + @foreach(['robust', 'mini', 'control_d'] as $typeKey) +{{ $inceptTypes[$typeKey]['allocated'] ?? 0 }} | + @endforeach +
| Submitted | + @foreach(['robust', 'mini', 'control_d'] as $typeKey) +{{ $inceptTypes[$typeKey]['used'] ?? 0 }} | + @endforeach +
| Balance | + @foreach(['robust', 'mini', 'control_d'] as $typeKey) +{{ $inceptTypes[$typeKey]['balance'] ?? 0 }} | + @endforeach +
No nebulizer allocation is set for this employee.
+ @else + + @endif +Select a doctor to see how many nebulizers are left for that MSL code (limit 10).
+