diff --git a/app/Http/Controllers/Business/SystemizationTodos.php b/app/Http/Controllers/Business/SystemizationTodos.php
index dbe64d81..a3b2f732 100644
--- a/app/Http/Controllers/Business/SystemizationTodos.php
+++ b/app/Http/Controllers/Business/SystemizationTodos.php
@@ -685,7 +685,9 @@ class SystemizationTodos extends Controller
'owner' => ['nullable', 'string', 'max:255'],
'assignee' => ['nullable', 'string', 'max:255'],
'start_date' => ['nullable', 'date_format:Y-m-d'],
+ 'start_note' => ['nullable', 'string', 'max:50'],
'due_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'],
+ 'due_note' => ['nullable', 'string', 'max:50'],
'status' => ['required', Rule::in(SystemizationTodo::STATUSES)],
'completed_at' => ['nullable', 'date_format:Y-m-d'],
'progress' => ['nullable', 'string', 'max:10000'],
diff --git a/resources/views/business/pages/systemization-todos.blade.php b/resources/views/business/pages/systemization-todos.blade.php
index 7860d04b..10880aa0 100644
--- a/resources/views/business/pages/systemization-todos.blade.php
+++ b/resources/views/business/pages/systemization-todos.blade.php
@@ -104,6 +104,13 @@
.sys-todo-field input, .sys-todo-field select, .sys-todo-field textarea { width:100%; padding:.5rem .6rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; color:#171717; font:inherit; font-size:.8125rem; }
.sys-todo-field input, .sys-todo-field select { min-height:2.15rem; }
.sys-todo-field textarea { min-height:5.5rem; resize:vertical; line-height:1.55; overflow:hidden; }
+ .sys-todo-date-control { display:flex; align-items:center; gap:.45rem; min-width:0; }
+ .sys-todo-date-control input[type="date"] { flex:1 1 auto; min-width:0; }
+ .sys-todo-date-control.is-pending input[type="date"] { background:#f7f0de; color:#777; }
+ .sys-todo-date-pending { display:inline-flex; flex:0 0 auto; align-items:center; gap:.3rem; min-height:2.15rem; padding:0 .55rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fcfaf5; color:#51483c; font-size:.75rem; white-space:nowrap; cursor:pointer; }
+ .sys-todo-date-pending input[type="checkbox"] { width:1rem; height:1rem; min-height:0; padding:0; accent-color:var(--sys-gold); }
+ .sys-todo-date-note { color:#856632; font-size:.68rem; line-height:1.4; }
+ .sys-todo-date-note[hidden] { display:none; }
.sys-todo-new-category { margin-top:.35rem; }
.sys-todo-new-category[hidden] { display:none; }
.sys-todo-owner-checks { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.2rem .45rem; max-height:7rem; padding:.4rem; border:1px solid var(--sys-border); border-radius:.45rem; background:#fff; overflow:auto; }
@@ -362,7 +369,9 @@
'owner' => $item->owner ?? '',
'assignee' => $item->assignee ?? '',
'start_date' => $item->start_date?->format('Y-m-d') ?? '',
+ 'start_note' => $item->start_note ?? '',
'due_date' => $item->due_date?->format('Y-m-d') ?? '',
+ 'due_note' => $item->due_note ?? '',
'status' => $item->status,
'completed_at' => $item->completed_at?->format('Y-m-d') ?? '',
'progress' => $item->progress ?? '',
@@ -525,8 +534,30 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -627,6 +658,20 @@
var sectionValue = dialog.querySelector('[data-sys-todo-section-value]');
var newSectionWrap = dialog.querySelector('[data-sys-todo-new-section]');
var newSectionInput = dialog.querySelector('[data-sys-todo-new-section-input]');
+ var startDateControl = {
+ input: form.querySelector('[name="start_date"]'),
+ pending: dialog.querySelector('[data-sys-todo-date-pending="start"]'),
+ note: form.querySelector('[data-sys-todo-date-note="start"]'),
+ caption: dialog.querySelector('[data-sys-todo-date-note-caption="start"]'),
+ wrap: dialog.querySelector('[data-sys-todo-date-control="start"]'),
+ };
+ var dueDateControl = {
+ input: form.querySelector('[name="due_date"]'),
+ pending: dialog.querySelector('[data-sys-todo-date-pending="due"]'),
+ note: form.querySelector('[data-sys-todo-date-note="due"]'),
+ caption: dialog.querySelector('[data-sys-todo-date-note-caption="due"]'),
+ wrap: dialog.querySelector('[data-sys-todo-date-control="due"]'),
+ };
var positionField = dialog.querySelector('[data-sys-todo-position-field]');
var positionSelect = dialog.querySelector('[data-sys-todo-position]');
var ownerValue = dialog.querySelector('[data-sys-todo-owner-value]');
@@ -671,6 +716,47 @@
if (field) field.value = value || '';
}
+ function updateDateNoteCaption(control) {
+ var note = control.note.value.trim();
+ var showPreserved = !control.pending.checked
+ && control.input.value === ''
+ && note !== ''
+ && note !== '待定';
+ control.caption.hidden = !showPreserved;
+ control.caption.textContent = showPreserved ? '原时间备注:' + note : '';
+ }
+
+ function syncDateControl(control, fromToggle) {
+ control.wrap.classList.toggle('is-pending', control.pending.checked);
+ if (control.pending.checked) {
+ control.input.value = '';
+ control.input.disabled = true;
+ control.note.value = '待定';
+ updateDateNoteCaption(control);
+ return;
+ }
+
+ control.input.disabled = false;
+ if (control.input.value !== '') {
+ control.note.value = '';
+ control.input.dataset.originalNote = '';
+ } else if (fromToggle) {
+ var originalNote = control.input.dataset.originalNote || '';
+ control.note.value = originalNote !== '待定' ? originalNote : '';
+ }
+ updateDateNoteCaption(control);
+ }
+
+ function applyDateControl(control, dateValue, noteValue) {
+ var normalizedDate = String(dateValue || '');
+ var normalizedNote = String(noteValue || '');
+ control.input.value = normalizedDate;
+ control.input.dataset.originalNote = normalizedNote;
+ control.note.value = normalizedNote;
+ control.pending.checked = normalizedDate === '' && normalizedNote === '待定';
+ syncDateControl(control, false);
+ }
+
function resizeTextarea(field) {
if (!field || field.offsetParent === null) return;
field.style.height = 'auto';
@@ -974,8 +1060,8 @@
setValue('title', payload.title);
setOwnerValue(payload.owner);
setValue('assignee', payload.assignee);
- setValue('start_date', payload.start_date);
- setValue('due_date', payload.due_date);
+ applyDateControl(startDateControl, payload.start_date, payload.start_note);
+ applyDateControl(dueDateControl, payload.due_date, payload.due_note);
setValue('status', payload.status);
setValue('completed_at', payload.completed_at);
setValue('progress', payload.progress);
@@ -984,6 +1070,8 @@
setCategoryValue('');
refreshSectionOptions('');
setOwnerValue('');
+ applyDateControl(startDateControl, '', '');
+ applyDateControl(dueDateControl, '', '');
}
if (!currentTodoId && !(payload && payload.status)) {
setValue('status', '待办');
@@ -1155,6 +1243,19 @@
populatePositionOptions();
}
});
+ [startDateControl, dueDateControl].forEach(function (control) {
+ control.pending.addEventListener('change', function () {
+ syncDateControl(control, true);
+ });
+ ['input', 'change'].forEach(function (eventName) {
+ control.input.addEventListener(eventName, function () {
+ if (control.input.value !== '') {
+ control.pending.checked = false;
+ }
+ syncDateControl(control, false);
+ });
+ });
+ });
dialog.addEventListener('input', function (event) {
if (event.target.matches('[data-sys-todo-auto-grow]')) resizeTextarea(event.target);
});
diff --git a/tests/Feature/Business/SystemizationTodosTest.php b/tests/Feature/Business/SystemizationTodosTest.php
index 9e8bc626..c9d782b5 100644
--- a/tests/Feature/Business/SystemizationTodosTest.php
+++ b/tests/Feature/Business/SystemizationTodosTest.php
@@ -32,7 +32,10 @@ class SystemizationTodosTest extends FeatureTestCase
->assertSeeText('已逾期')
->assertSeeText('开始时间')
->assertSeeText('完成时间')
+ ->assertSeeText('待定')
->assertSee('data-sys-todo-create', false)
+ ->assertSee('data-sys-todo-date-pending="start"', false)
+ ->assertSee('data-sys-todo-date-pending="due"', false)
->assertSee('data-sys-todo-owner-filter', false)
->assertSee('data-sys-todo-assignee-filter', false)
->assertSee('name="owner[]"', false)
@@ -358,6 +361,52 @@ class SystemizationTodosTest extends FeatureTestCase
}
}
+ public function test_manual_edit_saves_and_preserves_pending_date_notes(): void
+ {
+ $this->loginAs();
+
+ $this->post(route('business.systemization-todos.store'), [
+ 'category' => '收入',
+ 'section' => '平台收入核对',
+ 'title' => '待确认开始与完成日期',
+ 'owner' => '晨曦',
+ 'assignee' => '栎阳',
+ 'start_date' => null,
+ 'start_note' => '待定',
+ 'due_date' => null,
+ 'due_note' => '待定',
+ 'status' => SystemizationTodo::STATUS_PENDING,
+ 'progress' => '',
+ 'notes' => '',
+ ])->assertRedirect();
+
+ $todo = SystemizationTodo::query()->firstOrFail();
+ $this->assertNull($todo->start_date);
+ $this->assertSame('待定', $todo->start_note);
+ $this->assertNull($todo->due_date);
+ $this->assertSame('待定', $todo->due_note);
+
+ $this->patch(route('business.systemization-todos.update', ['todo' => $todo->id]), [
+ 'category' => '收入',
+ 'section' => '平台收入核对',
+ 'title' => '待确认开始与完成日期(已更新)',
+ 'owner' => '晨曦',
+ 'assignee' => '栎阳',
+ 'start_date' => null,
+ 'start_note' => '待定',
+ 'due_date' => null,
+ 'due_note' => '11月',
+ 'status' => SystemizationTodo::STATUS_IN_PROGRESS,
+ 'progress' => '等待平台确认',
+ 'notes' => '',
+ ])->assertRedirect();
+
+ $todo->refresh();
+ $this->assertSame('待确认开始与完成日期(已更新)', $todo->title);
+ $this->assertSame('待定', $todo->start_note);
+ $this->assertSame('11月', $todo->due_note);
+ }
+
public function test_page_filters_by_date_and_merges_adjacent_category_and_section_cells(): void
{
$this->loginAs();