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
$routes->group('', ['filter' => 'role:0,1,2,3,4'], function ($routes) {
$routes->post('collect/(:any)/(:any)', 'SamplesController::collect/$1/$2');
$routes->delete('collect/(:any)/(:any)', 'SamplesController::uncollect/$1/$2');
$routes->get('(:any)', 'SamplesController::show/$1');
});

View File

@ -68,14 +68,26 @@ class SamplesController extends BaseController
{
$db = \Config\Database::connect();
$userid = session('userid');
$sql = "update GDC_CMOD.dbo.TUBES set USERID='$userid',STATUS='1', COLLECTIONDATE=getdate() where ACCESSNUMBER='$accessnumber' and TUBENUMBER='$samplenumber'";
$db->query($sql);
$sql = "update GDC_CMOD.dbo.TUBES set USERID=?, STATUS='1', COLLECTIONDATE=getdate() where ACCESSNUMBER=? and TUBENUMBER=?";
$db->query($sql, [$userid, $accessnumber, $samplenumber]);
$sql = "INSERT INTO GDC_CMOD.dbo.AUDIT_TUBES(ACCESSNUMBER, TUBENUMBER, USERID, STATUS, LOGDATE)
VALUES ('$accessnumber', '$samplenumber', '$userid', '1', getdate())";
$db->query($sql);
VALUES (?, ?, ?, '1', getdate())";
$db->query($sql, [$accessnumber, $samplenumber, $userid]);
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)
{
$db = \Config\Database::connect();

View File

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