From ce802447e11922b99436503d2d8ecc187ef6f168 Mon Sep 17 00:00:00 2001 From: Ravi Gulhane Date: Tue, 29 Sep 2026 12:09:25 +0530 Subject: [PATCH] Track each employee's Incept nebulizer allocation and reject requests that exceed their balance or the MSL cap of 10. Co-authored-by: Cursor --- app/Exceptions/InceptNebLimitException.php | 7 + app/Http/Controllers/FormController.php | 80 +++++- app/Services/InceptNebAllocation.php | 269 ++++++++++++++++++ ..._09_29_000001_create_incept_neb_tables.php | 44 +++ .../seeders/InceptNebAllocationSeeder.php | 62 ++++ ...1cf4-f562-4eb7-8d53-efedd0133f41.blade.php | 148 +++++++++- routes/web.php | 1 + 7 files changed, 598 insertions(+), 13 deletions(-) create mode 100644 app/Exceptions/InceptNebLimitException.php create mode 100644 app/Services/InceptNebAllocation.php create mode 100644 database/migrations/2026_09_29_000001_create_incept_neb_tables.php create mode 100644 database/seeders/InceptNebAllocationSeeder.php 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 @@
Submitted By: {{$form->user_info->name}}
@endif + @php + $incept = (isset($additional) && isset($additional['incept_neb'])) ? $additional['incept_neb'] : null; + $inceptTypes = $incept['types'] ?? []; + @endphp + +
+
+ + + + + @foreach(['robust', 'mini', 'control_d'] as $typeKey) + + @endforeach + + + + + + @foreach(['robust', 'mini', 'control_d'] as $typeKey) + + @endforeach + + + + @foreach(['robust', 'mini', 'control_d'] as $typeKey) + + @endforeach + + + + @foreach(['robust', 'mini', 'control_d'] as $typeKey) + + @endforeach + + +
{{ $inceptTypes[$typeKey]['label'] ?? $typeKey }}
Allocation{{ $inceptTypes[$typeKey]['allocated'] ?? 0 }}
Submitted{{ $inceptTypes[$typeKey]['used'] ?? 0 }}
Balance{{ $inceptTypes[$typeKey]['balance'] ?? 0 }}
+
+ @if(empty($incept['has_allocation'])) +

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).

+
+ @csrf @if($id) @@ -53,7 +108,7 @@ @@ -227,6 +282,7 @@ $('#field-contact_id').change(function() { $('#field-msl_code').val($('option:selected', this).attr('msl_code')) + refreshInceptMsl(); }) /* $('#field-msl_code,#field-speciality').attr('readonly',true); */ @@ -268,14 +324,89 @@ }); + function inceptTypeKey(label) { + var value = (label || '').toLowerCase().replace(/[\s-]+/g, '_'); + if (value === 'robust') return 'robust'; + if (value === 'mini') return 'mini'; + if (value === 'control_d' || value === 'controld') return 'control_d'; + return ''; + } + + function paintInceptAllocation(allocation) { + if (!allocation) return; + $('#incept-emp-input').val(allocation.emp_id || ''); + ['robust', 'mini', 'control_d'].forEach(function(key) { + var row = (allocation.types && allocation.types[key]) ? allocation.types[key] : {allocated: 0, used: 0, balance: 0}; + $('.incept-allocated[data-type="' + key + '"]').text(row.allocated); + $('.incept-used[data-type="' + key + '"]').text(row.used); + $('.incept-balance[data-type="' + key + '"]').text(row.balance); + $('#incept-neb-panel').attr('data-' + key.replace('_', '-'), row.balance); + }); + if (!allocation.has_allocation) { + $('#incept-missing').text('No nebulizer allocation is set for this employee.').show(); + } else { + $('#incept-missing').hide(); + } + } + + function paintInceptMsl(msl) { + if (!msl || !msl.msl_code) { + $('#incept-msl-note').text('Select a doctor to see how many nebulizers are left for that MSL code (limit 10).'); + $('#incept-neb-panel').attr('data-msl-remaining', 10); + return; + } + $('#incept-neb-panel').attr('data-msl-remaining', msl.remaining); + $('#incept-msl-note').text('MSL ' + msl.msl_code + ' has ' + msl.used + ' of ' + msl.limit + ' nebulizers used. ' + msl.remaining + ' remaining.'); + } + + function refreshInceptBalance(mslCode) { + var userId = $('#field-user_id').length ? $('#field-user_id').val() : $('input[name="user_id"]').val(); + $.get('/form/inceptnebbalance', { + user_id: userId || '', + emp_id: $('#incept-emp-input').val() || '', + msl_code: mslCode || $('#field-msl_code').val() || '' + }, function(data) { + if (data && data.allocation) paintInceptAllocation(data.allocation); + if (data && data.msl) paintInceptMsl(data.msl); + }); + } + + function refreshInceptMsl() { + refreshInceptBalance($('#field-msl_code').val() || ''); + } + + $('#field-msl_code').on('change', refreshInceptMsl); + + $('#field-user_id').on('change', function() { + var code = $('option:selected', this).attr('employee_code') || ''; + $('#incept-emp-input').val(code); + refreshInceptBalance(''); + }); + $('#report-form').on('submit', function(event){ event.preventDefault(); - const formData = new FormData($('#report-form')[0]); - $('#report-form').append(''); - // Log form data to console - for (let [key, value] of formData.entries()) { - console.log(key, value); + var typeKey = inceptTypeKey($('#field-type_of_nebulizer').val()); + var qty = parseInt($('#field-no_of_nebulizer').val(), 10) || 0; + var balance = parseInt($('#incept-neb-panel').attr('data-' + typeKey.replace('_', '-')), 10); + var mslRemaining = parseInt($('#incept-neb-panel').attr('data-msl-remaining'), 10); + if (!typeKey) { + Swal.fire({ icon: 'error', title: 'Data not submitted', text: 'Select a nebulizer type.' }); + return; } + if (qty < 1) { + Swal.fire({ icon: 'error', title: 'Data not submitted', text: 'Enter the number of nebulizers.' }); + return; + } + if (isNaN(balance) || qty > balance) { + Swal.fire({ icon: 'error', title: 'Data not submitted', text: 'Only ' + (isNaN(balance) ? 0 : balance) + ' nebulizer(s) of this type are left in your allocation.' }); + return; + } + if (!isNaN(mslRemaining) && qty > mslRemaining) { + Swal.fire({ icon: 'error', title: 'Data not submitted', text: 'This MSL code can take only ' + mslRemaining + ' more nebulizer(s). The overall limit is 10.' }); + return; + } + $('#report-form input.incept-type-qty').remove(); + $('#report-form').append(''); $("#pageloader").fadeIn(); $.ajax({ type: "POST", @@ -291,15 +422,16 @@ text: 'Your data is successfully submitted!', icon: 'success' }).then(() => { - location.reload(); // 🔥 page refresh here + location.reload(); }); $('#report-form').trigger("reset"); } else { Swal.fire({ icon: 'error', title: 'Data not submitted', - text: 'Something went wrong, please try again after some time!' + text: (data && data.message) ? data.message : 'Something went wrong, please try again after some time!' }) + refreshInceptBalance($('#field-msl_code').val() || ''); } }, }); diff --git a/routes/web.php b/routes/web.php index 3c1aa3cd..5fd130a5 100755 --- a/routes/web.php +++ b/routes/web.php @@ -318,6 +318,7 @@ Route::get('/form/downloadpersonalization', [FormController::class, 'downloadper Route::get('/form/downloadpersonalizationzip/{files}', [FormController::class, 'downloadpersonalizationzip']); Route::middleware('throttle:10,1')->get('/form/downloaddata', [FormController::class, 'downloaddata']); +Route::get('/form/inceptnebbalance', [FormController::class, 'inceptNebBalance']); Route::get('/form/nebmitraverification', [FormController::class, 'nebmitraverification']); Route::get('/form/respidoctorsearch', [FormController::class, 'respidoctorsearch']);