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();