Add uncollect feature and security improvements

- Add DELETE /api/samples/collect/:accessnumber/:samplenumber endpoint
- Convert SQL queries to parameterized queries for security
- Add uncollect() method to SamplesController
- Update collect view to support uncollecting samples
- Fix checkbox logic to allow toggling collected samples
- Update hasChanges() to detect both collect and uncollect actions
This commit is contained in:
mahdahar 2026-03-04 13:12:21 +07:00
parent ee68d22d15
commit c2d66d0082
3 changed files with 38 additions and 18 deletions

View File

@ -52,6 +52,7 @@ $routes->get('(:any)/audit', 'ApiRequestsAuditController::show/$1');
// Collect & Show - All Roles // Collect & Show - All Roles
$routes->group('', ['filter' => 'role:0,1,2,3,4'], function ($routes) { $routes->group('', ['filter' => 'role:0,1,2,3,4'], function ($routes) {
$routes->post('collect/(:any)/(:any)', 'SamplesController::collect/$1/$2'); $routes->post('collect/(:any)/(:any)', 'SamplesController::collect/$1/$2');
$routes->delete('collect/(:any)/(:any)', 'SamplesController::uncollect/$1/$2');
$routes->get('(:any)', 'SamplesController::show/$1'); $routes->get('(:any)', 'SamplesController::show/$1');
}); });

View File

@ -68,14 +68,26 @@ class SamplesController extends BaseController
{ {
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$userid = session('userid'); $userid = session('userid');
$sql = "update GDC_CMOD.dbo.TUBES set USERID='$userid',STATUS='1', COLLECTIONDATE=getdate() where ACCESSNUMBER='$accessnumber' and TUBENUMBER='$samplenumber'"; $sql = "update GDC_CMOD.dbo.TUBES set USERID=?, STATUS='1', COLLECTIONDATE=getdate() where ACCESSNUMBER=? and TUBENUMBER=?";
$db->query($sql); $db->query($sql, [$userid, $accessnumber, $samplenumber]);
$sql = "INSERT INTO GDC_CMOD.dbo.AUDIT_TUBES(ACCESSNUMBER, TUBENUMBER, USERID, STATUS, LOGDATE) $sql = "INSERT INTO GDC_CMOD.dbo.AUDIT_TUBES(ACCESSNUMBER, TUBENUMBER, USERID, STATUS, LOGDATE)
VALUES ('$accessnumber', '$samplenumber', '$userid', '1', getdate())"; VALUES (?, ?, ?, '1', getdate())";
$db->query($sql); $db->query($sql, [$accessnumber, $samplenumber, $userid]);
return $this->respondCreated(['status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber"], 201); return $this->respondCreated(['status' => 'success', 'message' => 'Data updated successfully', 'data' => "$accessnumber-$samplenumber"], 201);
} }
public function uncollect($accessnumber, $samplenumber)
{
$db = \Config\Database::connect();
$userid = session('userid');
$sql = "update GDC_CMOD.dbo.TUBES set STATUS='0', COLLECTIONDATE=NULL where ACCESSNUMBER=? and TUBENUMBER=?";
$db->query($sql, [$accessnumber, $samplenumber]);
$sql = "INSERT INTO GDC_CMOD.dbo.AUDIT_TUBES(ACCESSNUMBER, TUBENUMBER, USERID, STATUS, LOGDATE)
VALUES (?, ?, ?, '0', getdate())";
$db->query($sql, [$accessnumber, $samplenumber, $userid]);
return $this->respond(['status' => 'success', 'message' => 'Sample uncollected successfully', 'data' => "$accessnumber-$samplenumber"], 200);
}
public function unreceive($accessnumber, $samplenumber) public function unreceive($accessnumber, $samplenumber)
{ {
$db = \Config\Database::connect(); $db = \Config\Database::connect();

View File

@ -100,13 +100,11 @@ $roleConfig = $config['phlebo'];
</thead> </thead>
<tbody> <tbody>
<template x-for="sample in samples" :key="sample.sampcode"> <template x-for="sample in samples" :key="sample.sampcode">
<tr :class="sample.colstatus == 1 ? 'bg-success/10' : (sample.selected ? 'bg-warning/10' : '')"> <tr :class="sample.selected ? (sample.colstatus == 1 ? 'bg-success/10' : 'bg-warning/10') : ''">
<td class="text-center p-2"> <td class="text-center p-2">
<input type="checkbox" <input type="checkbox"
class="checkbox checkbox-xs checkbox-primary" class="checkbox checkbox-xs checkbox-primary"
x-model="sample.selected" x-model="sample.selected">
:checked="sample.colstatus == 1"
:disabled="sample.colstatus == 1">
</td> </td>
<td class="font-mono text-xs p-2" x-text="sample.sampcode"></td> <td class="font-mono text-xs p-2" x-text="sample.sampcode"></td>
<td class="text-xs p-2" x-text="sample.name"></td> <td class="text-xs p-2" x-text="sample.name"></td>
@ -141,7 +139,7 @@ $roleConfig = $config['phlebo'];
</button> </button>
<button class="btn btn-sm btn-primary" <button class="btn btn-sm btn-primary"
@click="saveCollection()" @click="saveCollection()"
:disabled="isSaving || !hasSelectedSamples()" :disabled="isSaving || !hasChanges()"
:class="isSaving ? 'loading' : ''"> :class="isSaving ? 'loading' : ''">
<i class="fa fa-save mr-1" x-show="!isSaving"></i> <i class="fa fa-save mr-1" x-show="!isSaving"></i>
<span x-text="isSaving ? 'Saving' : 'Save'"></span> <span x-text="isSaving ? 'Saving' : 'Save'"></span>
@ -227,6 +225,12 @@ $roleConfig = $config['phlebo'];
return this.samples.some(s => s.selected && s.colstatus != 1); return this.samples.some(s => s.selected && s.colstatus != 1);
}, },
hasChanges() {
const toCollect = this.samples.some(s => s.selected && s.colstatus != 1);
const toUncollect = this.samples.some(s => !s.selected && s.colstatus == 1);
return toCollect || toUncollect;
},
hasUncollectedSamples() { hasUncollectedSamples() {
return this.samples.some(s => s.colstatus != 1 && !s.selected); return this.samples.some(s => s.colstatus != 1 && !s.selected);
}, },
@ -271,15 +275,17 @@ $roleConfig = $config['phlebo'];
try { try {
const samplesToCollect = this.samples.filter(s => s.selected && s.colstatus != 1); const samplesToCollect = this.samples.filter(s => s.selected && s.colstatus != 1);
const samplesToUncollect = this.samples.filter(s => !s.selected && s.colstatus == 1);
for (const sample of samplesToCollect) { for (const sample of samplesToCollect) {
await fetch(`${BASEURL}/api/samples/collect/${this.patient.accessnumber}`, { await fetch(`${BASEURL}/api/samples/collect/${this.patient.accessnumber}/${sample.sampcode}`, {
method: 'POST', method: 'POST'
headers: { 'Content-Type': 'application/json' }, });
body: JSON.stringify({ }
samplenumber: sample.sampcode,
userid: this.userid for (const sample of samplesToUncollect) {
}) await fetch(`${BASEURL}/api/samples/collect/${this.patient.accessnumber}/${sample.sampcode}`, {
method: 'DELETE'
}); });
} }
@ -287,10 +293,11 @@ $roleConfig = $config['phlebo'];
await this.saveComment(); await this.saveComment();
} }
this.showToast(`Collected ${samplesToCollect.length} sample(s)`, 'success'); const totalChanged = samplesToCollect.length + samplesToUncollect.length;
this.showToast(`Updated ${totalChanged} sample(s)`, 'success');
setTimeout(() => { setTimeout(() => {
this.resetForm(); this.fetchPatientData();
}, 1000); }, 1000);
} catch (error) { } catch (error) {
console.error('Error:', error); console.error('Error:', error);