Add ability to delete results on daily entry page

- Add delete/restore button in action column for each test row

- Implement soft delete functionality using deletedResults tracking array

- Update save endpoint to process deletedIds for soft deletion

- Allow saving when only deletions exist (no new data needed)
This commit is contained in:
mahdahar 2026-02-23 09:14:50 +07:00
parent 01a3e5d5bf
commit 448186bea7
2 changed files with 89 additions and 8 deletions

View File

@ -195,11 +195,18 @@ class EntryApiController extends BaseController
$date = $input['date']; $date = $input['date'];
$results = $input['results']; $results = $input['results'];
$deletedIds = $input['deletedIds'] ?? [];
$savedIds = []; $savedIds = [];
// Start transaction // Start transaction
$this->resultModel->db->transBegin(); $this->resultModel->db->transBegin();
// Handle deletions first
foreach ($deletedIds as $resultId) {
$this->resultModel->delete((int) $resultId);
}
// Save/update results
foreach ($results as $r) { foreach ($results as $r) {
if (!isset($r['controlId']) || !isset($r['testId']) || !isset($r['value'])) { if (!isset($r['controlId']) || !isset($r['testId']) || !isset($r['value'])) {
continue; continue;
@ -228,7 +235,7 @@ class EntryApiController extends BaseController
return $this->respond([ return $this->respond([
'status' => 'success', 'status' => 'success',
'message' => 'Saved ' . count($savedIds) . ' results', 'message' => 'Saved ' . count($savedIds) . ' results' . (count($deletedIds) > 0 ? ', deleted ' . count($deletedIds) : ''),
'data' => ['savedIds' => $savedIds] 'data' => ['savedIds' => $savedIds]
], 200); ], 200);
} catch (\Exception $e) { } catch (\Exception $e) {

View File

@ -100,8 +100,9 @@
<tr> <tr>
<th>Test</th> <th>Test</th>
<th class="text-center">Mean ± 2SD</th> <th class="text-center">Mean ± 2SD</th>
<th class="w-32">Result</th> <th class="w-40">Result</th>
<th class="w-56">Comment</th> <th class="w-56">Comment</th>
<th class="w-16">Action</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -123,18 +124,30 @@
step="0.01" step="0.01"
:placeholder="'...'" :placeholder="'...'"
class="input input-bordered input-sm w-full font-mono" class="input input-bordered input-sm w-full font-mono"
:class="getInputClass(test, $el.value)" :class="getInputClass(test, $el.value) + ' ' + (deletedResults.includes(test.testId) ? 'input-disabled' : '')"
:disabled="deletedResults.includes(test.testId)"
@input.debounce.300ms="updateResult(test.testId, $el.value)" @input.debounce.300ms="updateResult(test.testId, $el.value)"
:value="test.existingResult ? test.existingResult.resValue : ''"> :value="getResultValue(test.testId)">
</td> </td>
<td> <td>
<textarea <textarea
:placeholder="'Optional comment...'" :placeholder="'Optional comment...'"
rows="1" rows="1"
class="textarea textarea-bordered textarea-xs w-full" class="textarea textarea-bordered textarea-xs w-full"
:class="deletedResults.includes(test.testId) ? 'textarea-disabled' : ''"
:disabled="deletedResults.includes(test.testId)"
@input.debounce.300ms="updateComment(test.testId, $el.value)" @input.debounce.300ms="updateComment(test.testId, $el.value)"
:value="getComment(test.testId)"></textarea> :value="getComment(test.testId)"></textarea>
</td> </td>
<td class="text-center">
<button type="button"
@click="toggleDelete(test.testId)"
class="btn btn-ghost btn-sm"
:class="deletedResults.includes(test.testId) ? 'text-warning' : 'text-error'"
:title="deletedResults.includes(test.testId) ? 'Restore' : 'Delete'">
<i class="fa-solid" :class="deletedResults.includes(test.testId) ? 'fa-rotate-left' : 'fa-trash'"></i>
</button>
</td>
</tr> </tr>
</template> </template>
</tbody> </tbody>
@ -163,6 +176,7 @@ document.addEventListener('alpine:init', () => {
saving: false, saving: false,
resultsData: {}, resultsData: {},
commentsData: {}, commentsData: {},
deletedResults: [],
deptId: null, deptId: null,
departments: null, departments: null,
@ -236,9 +250,10 @@ document.addEventListener('alpine:init', () => {
const json = await response.json(); const json = await response.json();
this.tests = json.data || []; this.tests = json.data || [];
// Initialize resultsData and commentsData with existing values // Initialize resultsData, commentsData, and deletedResults
this.resultsData = {}; this.resultsData = {};
this.commentsData = {}; this.commentsData = {};
this.deletedResults = [];
for (const test of this.tests) { for (const test of this.tests) {
if (test.existingResult && test.existingResult.resValue !== null) { if (test.existingResult && test.existingResult.resValue !== null) {
this.resultsData[test.testId] = test.existingResult.resValue; this.resultsData[test.testId] = test.existingResult.resValue;
@ -263,7 +278,17 @@ document.addEventListener('alpine:init', () => {
this.saving = true; this.saving = true;
try { try {
const results = []; const results = [];
const deletedIds = [];
for (const test of this.tests) { for (const test of this.tests) {
// Check if this test is marked for deletion
if (this.deletedResults.includes(test.testId)) {
if (test.existingResult && test.existingResult.resultId) {
deletedIds.push(test.existingResult.resultId);
}
continue;
}
const value = this.resultsData[test.testId]; const value = this.resultsData[test.testId];
if (value !== undefined && value !== '') { if (value !== undefined && value !== '') {
results.push({ results.push({
@ -279,15 +304,20 @@ document.addEventListener('alpine:init', () => {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
date: this.date, date: this.date,
results: results results: results,
deletedIds: deletedIds
}) })
}); });
const json = await response.json(); const json = await response.json();
if (json.status === 'success') { if (json.status === 'success') {
// Save comments using the returned result IDs // Save comments for non-deleted tests
const savedIds = json.data.savedIds || []; const savedIds = json.data.savedIds || [];
for (const item of savedIds) { for (const item of savedIds) {
// Skip if this test is marked for deletion
if (this.deletedResults.includes(item.testId)) {
continue;
}
const comment = this.commentsData[item.testId]; const comment = this.commentsData[item.testId];
if (comment) { if (comment) {
await this.saveComment(item.testId, this.date, comment); await this.saveComment(item.testId, this.date, comment);
@ -299,6 +329,7 @@ document.addEventListener('alpine:init', () => {
// Refresh data // Refresh data
this.resultsData = {}; this.resultsData = {};
this.commentsData = {}; this.commentsData = {};
this.deletedResults = [];
await this.fetchTests(); await this.fetchTests();
} else { } else {
this.$dispatch('notify', { type: 'error', message: json.message || 'Failed to save' }); this.$dispatch('notify', { type: 'error', message: json.message || 'Failed to save' });
@ -329,12 +360,51 @@ document.addEventListener('alpine:init', () => {
} }
}, },
getResultValue(testId) {
// If marked for deletion, show empty
if (this.deletedResults.includes(testId)) {
return '';
}
// Return from resultsData if changed
if (testId in this.resultsData) {
return this.resultsData[testId];
}
// Return existing result from database
const test = this.tests.find(t => t.testId === testId);
if (test && test.existingResult && test.existingResult.resValue !== null) {
return test.existingResult.resValue;
}
return '';
},
updateResult(testId, value) { updateResult(testId, value) {
if (value === '') { if (value === '') {
delete this.resultsData[testId]; delete this.resultsData[testId];
} else { } else {
this.resultsData[testId] = value; this.resultsData[testId] = value;
} }
// Remove from deletedResults if user enters a value
if (value !== '' && this.deletedResults.includes(testId)) {
this.deletedResults = this.deletedResults.filter(id => id !== testId);
}
},
toggleDelete(testId) {
const test = this.tests.find(t => t.testId === testId);
const hasExistingResult = test && test.existingResult && test.existingResult.resValue !== null;
const hasChanges = testId in this.resultsData || testId in this.commentsData;
if (this.deletedResults.includes(testId)) {
// Restore - remove from deleted list
this.deletedResults = this.deletedResults.filter(id => id !== testId);
} else {
// Mark for deletion
if (hasExistingResult || hasChanges) {
this.deletedResults.push(testId);
// Clear any pending changes for this test
delete this.resultsData[testId];
}
}
}, },
updateComment(testId, value) { updateComment(testId, value) {
@ -384,7 +454,11 @@ document.addEventListener('alpine:init', () => {
}, },
get canSave() { get canSave() {
return this.selectedControl && (Object.keys(this.resultsData).length > 0 || Object.keys(this.commentsData).length > 0) && !this.saving; return this.selectedControl && (
Object.keys(this.resultsData).length > 0 ||
Object.keys(this.commentsData).length > 0 ||
this.deletedResults.length > 0
) && !this.saving;
}, },
setToday() { setToday() {