系统化待办时间支持待定 #655

Merged
yueyang merged 1 commits from feature/yueyang-systemization-todo-pending-dates into main 2026-10-26 21:18:05 +08:00
3 changed files with 156 additions and 4 deletions

View File

@ -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'],

View File

@ -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 @@
<input type="hidden" name="owner" data-sys-todo-owner-value>
</div>
<label class="sys-todo-field sys-todo-field--3">责任人<input name="assignee" maxlength="255" list="sysTodoAssignees"></label>
<label class="sys-todo-field sys-todo-field--3">开始时间<input type="date" name="start_date"></label>
<label class="sys-todo-field sys-todo-field--3">完成时间<input type="date" name="due_date"></label>
<div class="sys-todo-field sys-todo-field--3">
<label for="sysTodoStartDate">开始时间</label>
<div class="sys-todo-date-control" data-sys-todo-date-control="start">
<input id="sysTodoStartDate" type="date" name="start_date">
<label class="sys-todo-date-pending">
<input type="checkbox" data-sys-todo-date-pending="start">
<span>待定</span>
</label>
</div>
<input type="hidden" name="start_note" data-sys-todo-date-note="start">
<span class="sys-todo-date-note" data-sys-todo-date-note-caption="start" hidden></span>
</div>
<div class="sys-todo-field sys-todo-field--3">
<label for="sysTodoDueDate">完成时间</label>
<div class="sys-todo-date-control" data-sys-todo-date-control="due">
<input id="sysTodoDueDate" type="date" name="due_date">
<label class="sys-todo-date-pending">
<input type="checkbox" data-sys-todo-date-pending="due">
<span>待定</span>
</label>
</div>
<input type="hidden" name="due_note" data-sys-todo-date-note="due">
<span class="sys-todo-date-note" data-sys-todo-date-note-caption="due" hidden></span>
</div>
<label class="sys-todo-field sys-todo-field--6">待办事项<textarea name="title" maxlength="5000" required data-sys-todo-auto-grow></textarea></label>
<label class="sys-todo-field sys-todo-field--6">完成情况<textarea name="progress" maxlength="10000" data-sys-todo-auto-grow></textarea></label>
<label class="sys-todo-field sys-todo-field--6">备注<textarea name="notes" maxlength="10000" data-sys-todo-auto-grow></textarea></label>
@ -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);
});

View File

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