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

View File

@ -100,8 +100,9 @@
<tr>
<th>Test</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-16">Action</th>
</tr>
</thead>
<tbody>
@ -123,18 +124,30 @@
step="0.01"
:placeholder="'...'"
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)"
:value="test.existingResult ? test.existingResult.resValue : ''">
:value="getResultValue(test.testId)">
</td>
<td>
<textarea
:placeholder="'Optional comment...'"
rows="1"
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)"
:value="getComment(test.testId)"></textarea>
</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>
</template>
</tbody>
@ -163,6 +176,7 @@ document.addEventListener('alpine:init', () => {
saving: false,
resultsData: {},
commentsData: {},
deletedResults: [],
deptId: null,
departments: null,
@ -236,9 +250,10 @@ document.addEventListener('alpine:init', () => {
const json = await response.json();
this.tests = json.data || [];
// Initialize resultsData and commentsData with existing values
// Initialize resultsData, commentsData, and deletedResults
this.resultsData = {};
this.commentsData = {};
this.deletedResults = [];
for (const test of this.tests) {
if (test.existingResult && test.existingResult.resValue !== null) {
this.resultsData[test.testId] = test.existingResult.resValue;
@ -263,7 +278,17 @@ document.addEventListener('alpine:init', () => {
this.saving = true;
try {
const results = [];
const deletedIds = [];
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];
if (value !== undefined && value !== '') {
results.push({
@ -279,15 +304,20 @@ document.addEventListener('alpine:init', () => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
date: this.date,
results: results
results: results,
deletedIds: deletedIds
})
});
const json = await response.json();
if (json.status === 'success') {
// Save comments using the returned result IDs
// Save comments for non-deleted tests
const savedIds = json.data.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];
if (comment) {
await this.saveComment(item.testId, this.date, comment);
@ -299,6 +329,7 @@ document.addEventListener('alpine:init', () => {
// Refresh data
this.resultsData = {};
this.commentsData = {};
this.deletedResults = [];
await this.fetchTests();
} else {
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) {
if (value === '') {
delete this.resultsData[testId];
} else {
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) {
@ -384,7 +454,11 @@ document.addEventListener('alpine:init', () => {
},
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() {