系统化待办序号支持编辑并移除序号筛选 #626

Merged
yueyang merged 2 commits from fix/yueyang-systemization-todo-position-edit into main 2026-10-26 18:31:46 +08:00
3 changed files with 118 additions and 78 deletions

View File

@ -106,27 +106,6 @@ class SystemizationTodos extends Controller
});
$items = $items->sortBy('hierarchy_order')->values();
$positionGroups = $this->positionGroups($sequenceMap);
if ($filters['type_sequence'] > 0) {
$items = $items
->filter(static fn (SystemizationTodo $todo): bool =>
(int) $todo->type_sequence === $filters['type_sequence']
)
->values();
}
if ($filters['content_sequence'] > 0) {
$items = $items
->filter(static fn (SystemizationTodo $todo): bool =>
(int) $todo->content_sequence === $filters['content_sequence']
)
->values();
}
if ($filters['item_sequence'] > 0) {
$items = $items
->filter(static fn (SystemizationTodo $todo): bool =>
(int) $todo->item_sequence === $filters['item_sequence']
)
->values();
}
$base = SystemizationTodo::query();
$categories = SystemizationTodo::query()
@ -428,7 +407,11 @@ class SystemizationTodos extends Controller
);
}
private function contentSiblings(?string $category, ?string $section): Collection
private function contentSiblings(
?string $category,
?string $section,
?int $excludeId = null,
): Collection
{
$categoryKey = $this->hierarchyGroupKey($this->hierarchyLabel($category));
$sectionKey = $this->hierarchyGroupKey($this->hierarchyLabel($section));
@ -439,12 +422,54 @@ class SystemizationTodos extends Controller
->lockForUpdate()
->get()
->filter(fn (SystemizationTodo $todo): bool =>
$this->hierarchyGroupKey($this->hierarchyLabel($todo->category)) === $categoryKey
(int) $todo->id !== (int) $excludeId
&& $this->hierarchyGroupKey($this->hierarchyLabel($todo->category)) === $categoryKey
&& $this->hierarchyGroupKey($this->hierarchyLabel($todo->section)) === $sectionKey
)
->values();
}
private function positionForUpdate(
SystemizationTodo $todo,
int $targetPosition,
array &$data,
): void {
$currentCategoryKey = $this->hierarchyGroupKey($this->hierarchyLabel($todo->category));
$currentSectionKey = $this->hierarchyGroupKey($this->hierarchyLabel($todo->section));
$targetCategoryKey = $this->hierarchyGroupKey($this->hierarchyLabel($data['category'] ?? null));
$targetSectionKey = $this->hierarchyGroupKey($this->hierarchyLabel($data['section'] ?? null));
$sameGroup = $currentCategoryKey === $targetCategoryKey
&& $currentSectionKey === $targetSectionKey;
if ($sameGroup) {
$currentSiblings = $this->contentSiblings($todo->category, $todo->section);
$currentIndex = $currentSiblings->search(
static fn (SystemizationTodo $item): bool => (int) $item->id === (int) $todo->id
);
if ($currentIndex !== false && $targetPosition === ((int) $currentIndex + 1)) {
return;
}
}
$siblings = $this->contentSiblings(
$data['category'] ?? null,
$data['section'] ?? null,
(int) $todo->id,
);
if ($siblings->isEmpty()) {
$insertOrder = (int) SystemizationTodo::query()->max('sort_order') + 1;
} elseif ($targetPosition <= $siblings->count()) {
$insertOrder = (int) $siblings[$targetPosition - 1]->sort_order;
} else {
$insertOrder = (int) $siblings->max('sort_order') + 1;
}
SystemizationTodo::query()
->where('sort_order', '>=', $insertOrder)
->increment('sort_order');
$data['sort_order'] = $insertOrder;
}
private function groupTodoIds(SystemizationTodo $todo, bool $includeSection): array
{
$categoryKey = $this->hierarchyGroupKey($this->hierarchyLabel($todo->category));
@ -480,6 +505,7 @@ class SystemizationTodos extends Controller
public function update(Request $request, SystemizationTodo $todo): RedirectResponse
{
$data = $this->validated($request);
$targetPosition = max(0, (int) ($data['position'] ?? 0));
unset($data['position']);
$data['updated_by'] = user_id();
$this->normalizeCompletionDates($data);
@ -500,7 +526,16 @@ class SystemizationTodos extends Controller
}
}
$todo->update($data);
DB::transaction(function () use ($todo, $data, $targetPosition): void {
$lockedTodo = SystemizationTodo::query()
->lockForUpdate()
->findOrFail($todo->id);
if ($targetPosition > 0) {
$this->positionForUpdate($lockedTodo, $targetPosition, $data);
}
$lockedTodo->update($data);
$todo->setRawAttributes($lockedTodo->getAttributes(), true);
});
$this->storeAttachments($request, $todo);
return $this->backToPage('待办事项已更新并定位。', 'success', $todo);
@ -572,9 +607,6 @@ class SystemizationTodos extends Controller
'assignees' => $this->peopleFilter('assignee'),
'start_date' => $this->normalizeDateFilter(request('start_date')),
'due_date' => $this->normalizeDateFilter(request('due_date')),
'type_sequence' => max(0, (int) request('type_sequence', 0)),
'content_sequence' => max(0, (int) request('content_sequence', 0)),
'item_sequence' => max(0, (int) request('item_sequence', 0)),
'keyword' => trim((string) request('keyword', '')),
];
}

View File

@ -215,9 +215,6 @@
<option value="{{ $category }}" @selected($todoFilters['category'] === $category)>{{ $category }}</option>
@endforeach
</select></label>
<label>类型序号<input type="number" name="type_sequence" min="1" step="1" value="{{ $todoFilters['type_sequence'] ?: '' }}" placeholder="如:1"></label>
<label>内容序号<input type="number" name="content_sequence" min="1" step="1" value="{{ $todoFilters['content_sequence'] ?: '' }}" placeholder="如:2"></label>
<label>待办序号<input type="number" name="item_sequence" min="1" step="1" value="{{ $todoFilters['item_sequence'] ?: '' }}" placeholder="如:3"></label>
@php
$selectedOwners = $todoOwners->filter(
static fn (string $owner): bool => in_array($owner, $todoFilters['owners'], true)
@ -345,7 +342,7 @@
<table class="sys-todo-table" aria-label="财务系统化待办清单">
<thead>
<tr>
<th scope="col">类型</th><th scope="col">内容</th><th scope="col">待办序号</th><th scope="col">待办事项</th><th scope="col">总负责人</th><th scope="col">责任人</th>
<th scope="col">类型</th><th scope="col">内容</th><th scope="col">待办事项序号</th><th scope="col">待办事项</th><th scope="col">总负责人</th><th scope="col">责任人</th>
<th scope="col">开始时间</th><th scope="col">完成时间</th><th scope="col">状态</th><th scope="col">完成情况</th><th scope="col">完成截图</th><th scope="col">备注</th>
</tr>
</thead>
@ -363,6 +360,7 @@
};
$todoPayload = [
'id' => $item->id,
'position' => (int) $item->item_sequence,
'category' => $item->category ?? '',
'section' => $sectionLabels[$loop->index] ?? '',
'title' => $titleLabels[$loop->index] ?? '',
@ -489,7 +487,7 @@
</div>
</div>
<label class="sys-todo-field sys-todo-field--4">内容<textarea name="section" maxlength="255" rows="2" data-sys-todo-auto-grow></textarea></label>
<label class="sys-todo-field sys-todo-field--2 sys-todo-position-field" data-sys-todo-position-field>新增位置
<label class="sys-todo-field sys-todo-field--2 sys-todo-position-field" data-sys-todo-position-field>待办事项序号
<select name="position" data-sys-todo-position></select>
</label>
<label class="sys-todo-field sys-todo-field--3">状态<select name="status" required>@foreach ($todoStatuses as $status)<option value="{{ $status }}">{{ $status }}</option>@endforeach</select></label>
@ -675,28 +673,28 @@
function populatePositionOptions() {
positionSelect.replaceChildren();
positionSelect.disabled = !!currentTodoId;
if (currentTodoId) {
positionField.hidden = true;
return;
}
positionField.hidden = false;
positionSelect.disabled = false;
var group = currentPositionGroup();
var count = group ? Number(group.count || 0) : 0;
for (var index = 1; index <= count; index++) {
var positionCount = currentTodoId ? Math.max(count, 1) : count;
for (var index = 1; index <= positionCount; index++) {
var before = document.createElement('option');
before.value = String(index);
before.textContent = '第 ' + index + ' 条前插入(原第 ' + index + ' 条及之后顺延)';
before.textContent = currentTodoId
? '第 ' + index + ' 条'
: '第 ' + index + ' 条前插入(原第 ' + index + ' 条及之后顺延)';
positionSelect.appendChild(before);
}
var end = document.createElement('option');
end.value = String(count + 1);
end.textContent = count > 0
? '添加到末尾(第 ' + (count + 1) + ' 条)'
: '新增为第 1 条';
positionSelect.appendChild(end);
positionSelect.value = String(count + 1);
if (!currentTodoId) {
var end = document.createElement('option');
end.value = String(count + 1);
end.textContent = count > 0
? '添加到末尾(第 ' + (count + 1) + ' 条)'
: '新增为第 1 条';
positionSelect.appendChild(end);
positionSelect.value = String(count + 1);
}
}
function syncScrollRail() {
@ -866,7 +864,6 @@
form.action = currentTodoId ? routes.update.replace('__TODO__', currentTodoId) : routes.store;
title.textContent = currentTodoId ? '编辑待办' : '新增待办';
deleteButton.hidden = !currentTodoId;
positionField.hidden = !!currentTodoId;
if (payload) {
setCategoryValue(payload.category);
setValue('section', payload.section);
@ -887,6 +884,13 @@
setValue('status', '待办');
}
populatePositionOptions();
if (currentTodoId) {
var requestedPosition = Number((payload && payload.position) || 1);
positionSelect.value = String(Math.min(
Math.max(requestedPosition, 1),
Math.max(positionSelect.options.length, 1)
));
}
existing.replaceChildren();
((payload && payload.attachments) || []).forEach(function (attachment) {

View File

@ -28,9 +28,7 @@ class SystemizationTodosTest extends FeatureTestCase
->assertSeeText('从 Excel 同步待办')
->assertSeeText('总负责人')
->assertSeeText('责任人')
->assertSeeText('类型序号')
->assertSeeText('内容序号')
->assertSeeText('待办序号')
->assertSeeText('待办事项序号')
->assertSeeText('已逾期')
->assertSeeText('开始时间')
->assertSeeText('完成时间')
@ -43,6 +41,10 @@ class SystemizationTodosTest extends FeatureTestCase
->assertSee('data-sys-todo-category-select', false)
->assertSee('data-sys-todo-new-category-input', false)
->assertSee('data-sys-todo-position', false)
->assertSeeText('待办事项序号')
->assertDontSee('name="type_sequence"', false)
->assertDontSee('name="content_sequence"', false)
->assertDontSee('name="item_sequence"', false)
->assertSee('data-sys-todo-group-edit', false)
->assertSee('id="sysTodoGroupDialog"', false)
->assertDontSee('sys-todo-table__clamp', false)
@ -184,6 +186,34 @@ class SystemizationTodosTest extends FeatureTestCase
$items[6]->fresh()->sort_order,
$items[7]->fresh()->sort_order,
);
$this->patch(route('business.systemization-todos.update', ['todo' => $items[7]->id]), [
'category' => '收入',
'section' => '平台收入核对',
'position' => 2,
'title' => '原待办 7',
'owner' => '晨曦',
'assignee' => '栎阳',
'status' => SystemizationTodo::STATUS_PENDING,
])->assertRedirect();
$reordered = $this->get(route('business.pages.show', [
'section' => 'other',
'page' => 'systemization-todos',
]))->assertOk()->viewData('content')['items']
->where('category', '收入')
->where('section', '平台收入核对')
->values();
$this->assertSame([
'原待办 1',
'原待办 7',
'原待办 2',
'原待办 3',
'原待办 4',
'原待办 5',
'原待办 6',
'新增待办 7',
], $reordered->pluck('title')->all());
}
public function test_type_and_content_cells_can_be_edited_for_the_whole_group(): void
@ -278,7 +308,7 @@ class SystemizationTodosTest extends FeatureTestCase
$pageResponse
->assertOk()
->assertSee('rowspan="2"', false)
->assertSee('<th scope="col">内容</th><th scope="col">待办序号</th><th scope="col">待办事项</th>', false)
->assertSee('<th scope="col">内容</th><th scope="col">待办事项序号</th><th scope="col">待办事项</th>', false)
->assertSee('sys-todo-table__type', false)
->assertSee('title="类型序号">1</span>', false)
->assertSee('title="内容序号">1</span>', false)
@ -320,22 +350,6 @@ class SystemizationTodosTest extends FeatureTestCase
->all(),
);
$sequenceResponse = $this->get(route('business.pages.show', [
'section' => 'other',
'page' => 'systemization-todos',
'category' => '收入',
'item_sequence' => 2,
]))->assertOk();
$sequenceContent = $sequenceResponse->viewData('content');
$this->assertSame(['2、收入事项 B'], $sequenceContent['items']->pluck('title')->all());
$typeSequenceContent = $this->get(route('business.pages.show', [
'section' => 'other',
'page' => 'systemization-todos',
'type_sequence' => 2,
]))->assertOk()->viewData('content');
$this->assertSame(['3、成本事项'], $typeSequenceContent['items']->pluck('title')->all());
$highlightedTodo = $pageItems->firstWhere('title', '2、收入事项 B');
$this->get(route('business.pages.show', [
'section' => 'other',
@ -430,16 +444,6 @@ class SystemizationTodosTest extends FeatureTestCase
->values()
->all(),
);
$contentResponse = $this->get(route('business.pages.show', [
'section' => 'other',
'page' => 'systemization-todos',
'type_sequence' => 1,
'content_sequence' => 2,
]))->assertOk();
$this->assertSame(
[$secondContent->id],
$contentResponse->viewData('content')['items']->pluck('id')->all(),
);
$this->assertSame(
[$completed->id, $inProgress->id],
$items