US-017: Implement Daily Comments per day per test

- Add per-day comment input in monthly entry grid (comment icon per cell)
- Update saveMonthly() to save rescomment field for each result
- Update getMonthlyData() to return per-day comments
- Store comments in results.rescomment field with soft deletes support
This commit is contained in:
mahdahar 2026-01-16 17:22:40 +07:00
parent 1374c8769f
commit 2e23fc38c3
2 changed files with 63 additions and 11 deletions

View File

@ -98,6 +98,7 @@ class EntryApiController extends BaseController
foreach ($tests as $testData) { foreach ($tests as $testData) {
$testId = $testData['testId'] ?? 0; $testId = $testData['testId'] ?? 0;
$resvalues = $testData['resvalue'] ?? []; $resvalues = $testData['resvalue'] ?? [];
$rescomments = $testData['rescomment'] ?? [];
$controlTest = $this->controlTestModel->getByControlAndTest($controlId, $testId); $controlTest = $this->controlTestModel->getByControlAndTest($controlId, $testId);
$mean = $controlTest['mean'] ?? 0; $mean = $controlTest['mean'] ?? 0;
@ -111,12 +112,15 @@ class EntryApiController extends BaseController
foreach ($resvalues as $day => $value) { foreach ($resvalues as $day => $value) {
if (!empty($value)) { if (!empty($value)) {
$resdate = $yearMonth . '-' . str_pad($day, 2, '0', STR_PAD_LEFT);
$rescomment = $rescomments[$day] ?? '';
$resultData = [ $resultData = [
'control_ref_id' => $controlId, 'control_ref_id' => $controlId,
'test_ref_id' => $testId, 'test_ref_id' => $testId,
'resdate' => $yearMonth . '-' . str_pad($day, 2, '0', STR_PAD_LEFT), 'resdate' => $resdate,
'resvalue' => $value, 'resvalue' => $value,
'rescomment' => '', 'rescomment' => $rescomment,
]; ];
if ($operation === 'replace') { if ($operation === 'replace') {
@ -223,9 +227,13 @@ class EntryApiController extends BaseController
$comment = $this->commentModel->getByControlTestMonth($controlId, $testId, $yearMonth); $comment = $this->commentModel->getByControlTestMonth($controlId, $testId, $yearMonth);
$formValues = []; $formValues = [];
$comments = [];
foreach ($results as $row) { foreach ($results as $row) {
$day = (int)date('j', strtotime($row['resdate'])); $day = (int)date('j', strtotime($row['resdate']));
$formValues[$day] = $row['resvalue']; $formValues[$day] = $row['resvalue'];
if (!empty($row['rescomment'])) {
$comments[$day] = $row['rescomment'];
}
} }
return $this->respond([ return $this->respond([
@ -233,6 +241,7 @@ class EntryApiController extends BaseController
'message' => 'fetch success', 'message' => 'fetch success',
'data' => [ 'data' => [
'formValues' => $formValues, 'formValues' => $formValues,
'comments' => $comments,
'comment' => $comment ? $comment['comtext'] : '' 'comment' => $comment ? $comment['comtext'] : ''
] ]
], 200); ], 200);

View File

@ -108,15 +108,38 @@
<span x-text="getTestName(testId)"></span> <span x-text="getTestName(testId)"></span>
</td> </td>
<template x-for="day in daysInMonth" :key="day"> <template x-for="day in daysInMonth" :key="day">
<td class="py-1 px-1" :class="isWeekend(day) ? 'bg-red-50' : ''"> <td class="py-1 px-1 relative" :class="isWeekend(day) ? 'bg-red-50' : ''">
<input <div class="flex items-center gap-1">
type="number" <input
step="0.01" type="number"
class="w-full px-2 py-1.5 text-center text-sm bg-white border border-slate-200 rounded focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" step="0.01"
:placeholder="'-'" class="w-full px-2 py-1.5 text-center text-sm bg-white border border-slate-200 rounded focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500"
x-model="monthlyData[testId + '_" + day + "']" :placeholder="'-'"
@change="markDirty(testId, day)" x-model="monthlyData[testId + '_' + day]"
@change="markDirty(testId, day)"
>
<button
class="p-1 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
:title="monthlyData[testId + '_' + day + '_comment'] || 'Add comment'"
@click.stop="toggleComment(testId, day)"
>
<i class="fa-solid fa-comment text-xs" :class="monthlyData[testId + '_' + day + '_comment'] ? 'text-blue-500' : ''"></i>
</button>
</div>
<div
x-show="showCommentId === testId + '_' + day"
x-transition
class="absolute z-10 top-full left-0 mt-1 w-48"
@click.outside="showCommentId = null"
> >
<textarea
class="w-full px-2 py-1.5 text-xs bg-white border border-slate-200 rounded shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 resize-none"
rows="2"
placeholder="Add comment..."
x-model="monthlyData[testId + '_' + day + '_comment']"
@keydown.enter.prevent
></textarea>
</div>
</td> </td>
</template> </template>
</tr> </tr>
@ -191,6 +214,7 @@ document.addEventListener('alpine:init', () => {
dirtyCells: new Set(), dirtyCells: new Set(),
operation: 'replace', operation: 'replace',
saveResult: null, saveResult: null,
showCommentId: null,
init() { init() {
this.loadDraft(); this.loadDraft();
@ -230,6 +254,15 @@ document.addEventListener('alpine:init', () => {
this.dirtyCells.add(testId + '_' + day); this.dirtyCells.add(testId + '_' + day);
}, },
toggleComment(testId, day) {
const id = testId + '_' + day;
if (this.showCommentId === id) {
this.showCommentId = null;
} else {
this.showCommentId = id;
}
},
async loadControls() { async loadControls() {
this.errors = {}; this.errors = {};
this.controls = []; this.controls = [];
@ -311,9 +344,13 @@ document.addEventListener('alpine:init', () => {
const data = await response.json(); const data = await response.json();
if (data.status === 'success') { if (data.status === 'success') {
const formValues = data.data.formValues || {}; const formValues = data.data.formValues || {};
const comments = data.data.comments || {};
for (const [day, value] of Object.entries(formValues)) { for (const [day, value] of Object.entries(formValues)) {
this.monthlyData[testId + '_' + day] = value; this.monthlyData[testId + '_' + day] = value;
} }
for (const [day, comment] of Object.entries(comments)) {
this.monthlyData[testId + '_' + day + '_comment'] = comment;
}
if (!this.comment && data.data.comment) { if (!this.comment && data.data.comment) {
this.comment = data.data.comment; this.comment = data.data.comment;
} }
@ -354,17 +391,23 @@ document.addEventListener('alpine:init', () => {
const tests = []; const tests = [];
for (const testId of this.selectedTests) { for (const testId of this.selectedTests) {
const resvalue = {}; const resvalue = {};
const rescomment = {};
let hasData = false; let hasData = false;
for (let day = 1; day <= this.daysInMonth; day++) { for (let day = 1; day <= this.daysInMonth; day++) {
const key = testId + '_' + day; const key = testId + '_' + day;
const value = this.monthlyData[key]; const value = this.monthlyData[key];
const commentKey = testId + '_' + day + '_comment';
const comment = this.monthlyData[commentKey];
if (value !== undefined && value !== null && value !== '') { if (value !== undefined && value !== null && value !== '') {
resvalue[day] = value; resvalue[day] = value;
hasData = true; hasData = true;
} }
if (comment !== undefined && comment !== null && comment !== '') {
rescomment[day] = comment;
}
} }
if (hasData) { if (hasData) {
tests.push({ testId, resvalue }); tests.push({ testId, resvalue, rescomment });
} }
} }