系统化待办序号支持编辑并移除序号筛选 #626
@ -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', '')),
|
||||
];
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user