fix: 统一系统化待办总负责人映射 #694
@ -28,6 +28,22 @@ class SystemizationTodos extends Controller
|
||||
'pending' => '时间待定',
|
||||
'none' => '无提醒',
|
||||
];
|
||||
private const OWNER_OPTIONS = ['人参', '晨曦', '常仪', '待定'];
|
||||
private const OWNER_ALIASES = [
|
||||
'人参' => '人参',
|
||||
'苏木' => '人参',
|
||||
'李斯' => '人参',
|
||||
'周生辰' => '人参',
|
||||
'晨曦' => '晨曦',
|
||||
'栎阳' => '晨曦',
|
||||
'秋香' => '晨曦',
|
||||
'常仪' => '常仪',
|
||||
'玉兔' => '常仪',
|
||||
'山时' => '常仪',
|
||||
'林冲' => '常仪',
|
||||
'杳杳' => '常仪',
|
||||
'待定' => '待定',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly SystemizationTodoAuditLogger $audit,
|
||||
@ -74,11 +90,7 @@ class SystemizationTodos extends Controller
|
||||
if ($filters['owners'] !== []) {
|
||||
$query->where(function ($builder) use ($filters): void {
|
||||
foreach ($filters['owners'] as $owner) {
|
||||
$builder->orWhere(
|
||||
'owner',
|
||||
'like',
|
||||
'%' . addcslashes($owner, '%_\\') . '%',
|
||||
);
|
||||
$this->applyOwnerFilter($builder, $owner);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -138,6 +150,7 @@ class SystemizationTodos extends Controller
|
||||
$todo->setAttribute('type_count', $sequence['type_count']);
|
||||
$todo->setAttribute('content_count', $sequence['content_count']);
|
||||
$todo->setAttribute('status_label', $todo->statusLabel());
|
||||
$todo->setAttribute('owner_display', implode('、', $this->ownerNames($todo->owner)));
|
||||
$reminder = $todo->reminder();
|
||||
$todo->setAttribute('reminder_label', $reminder['label']);
|
||||
$todo->setAttribute('reminder_tone', $reminder['tone']);
|
||||
@ -178,7 +191,7 @@ class SystemizationTodos extends Controller
|
||||
'todoFilters' => $filters,
|
||||
'todoCategories' => $categories,
|
||||
'todoContentOptions' => $this->contentOptions(),
|
||||
'todoOwners' => $this->peopleOptions('owner'),
|
||||
'todoOwners' => collect(self::OWNER_OPTIONS),
|
||||
'todoAssignees' => $this->peopleOptions('assignee'),
|
||||
'todoStatuses' => SystemizationTodo::STATUSES,
|
||||
'todoStatusOptions' => $statusOptions,
|
||||
@ -376,6 +389,7 @@ class SystemizationTodos extends Controller
|
||||
{
|
||||
$data = $this->validated($request);
|
||||
$this->normalizePendingDates($request, $data);
|
||||
$this->normalizeOwnerData($data);
|
||||
$position = max(1, (int) ($data['position'] ?? 1));
|
||||
unset($data['position']);
|
||||
$data['company_id'] = (int) company_id();
|
||||
@ -580,6 +594,7 @@ class SystemizationTodos extends Controller
|
||||
$before = $this->audit->snapshot($todo, true);
|
||||
$data = $this->validated($request);
|
||||
$this->normalizePendingDates($request, $data);
|
||||
$this->normalizeOwnerData($data);
|
||||
$targetPosition = max(0, (int) ($data['position'] ?? 0));
|
||||
unset($data['position']);
|
||||
$data['updated_by'] = user_id();
|
||||
@ -765,7 +780,7 @@ class SystemizationTodos extends Controller
|
||||
'status' => $this->normalizeStatusValue(request('status')),
|
||||
'reminder' => $this->normalizeReminderValue(request('reminder')),
|
||||
'category' => trim((string) request('category', '')),
|
||||
'owners' => $this->peopleFilter('owner'),
|
||||
'owners' => $this->normalizeOwnerFilterValues($this->peopleFilter('owner')),
|
||||
'assignees' => $this->peopleFilter('assignee'),
|
||||
'start_date' => $this->normalizeDateValue(request('start_date')),
|
||||
'due_date' => $this->normalizeDateValue(request('due_date')),
|
||||
@ -821,6 +836,80 @@ class SystemizationTodos extends Controller
|
||||
->values();
|
||||
}
|
||||
|
||||
private function normalizeOwnerFilterValues(array $owners): array
|
||||
{
|
||||
return collect($owners)
|
||||
->map(static fn (string $owner): ?string => self::OWNER_ALIASES[$owner] ?? null)
|
||||
->filter()
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function applyOwnerFilter($builder, string $owner): void
|
||||
{
|
||||
$aliases = array_keys(array_filter(
|
||||
self::OWNER_ALIASES,
|
||||
static fn (string $canonical): bool => $canonical === $owner,
|
||||
));
|
||||
|
||||
if ($aliases !== []) {
|
||||
$builder->orWhere(function ($aliasQuery) use ($aliases): void {
|
||||
foreach ($aliases as $alias) {
|
||||
$aliasQuery->orWhere(
|
||||
'owner',
|
||||
'like',
|
||||
'%' . addcslashes($alias, '%_\\') . '%',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($owner !== '待定') {
|
||||
return;
|
||||
}
|
||||
|
||||
$knownAliases = array_values(array_diff(array_keys(self::OWNER_ALIASES), ['待定']));
|
||||
$builder->orWhere(function ($pendingQuery) use ($knownAliases): void {
|
||||
$pendingQuery->whereNull('owner')->orWhere('owner', '');
|
||||
if ($knownAliases === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pendingQuery->orWhere(function ($unknownQuery) use ($knownAliases): void {
|
||||
foreach ($knownAliases as $alias) {
|
||||
$unknownQuery->where(
|
||||
'owner',
|
||||
'not like',
|
||||
'%' . addcslashes($alias, '%_\\') . '%',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private function ownerNames(?string $value): array
|
||||
{
|
||||
$names = collect(preg_split('/[、,,;;\/]+/u', trim((string) $value)) ?: [])
|
||||
->map(static fn (string $name): string => trim($name))
|
||||
->filter(static fn (string $name): bool => $name !== '' && $name !== '—')
|
||||
->map(static fn (string $name): string => self::OWNER_ALIASES[$name] ?? '待定')
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return $names === [] ? ['待定'] : $names;
|
||||
}
|
||||
|
||||
private function normalizeOwnerData(array &$data): void
|
||||
{
|
||||
if (! array_key_exists('owner', $data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$data['owner'] = implode('、', $this->ownerNames($data['owner']));
|
||||
}
|
||||
|
||||
private function contentOptions(): array
|
||||
{
|
||||
$options = [];
|
||||
@ -979,6 +1068,7 @@ class SystemizationTodos extends Controller
|
||||
->take(50)
|
||||
->values()
|
||||
->all();
|
||||
$owners = $this->normalizeOwnerFilterValues($owners);
|
||||
$assignees = collect((array) ($filters['assignees'] ?? []))
|
||||
->filter(static fn (mixed $value): bool => is_string($value))
|
||||
->map(static fn (string $value): string => trim($value))
|
||||
|
||||
@ -498,7 +498,7 @@
|
||||
'category' => $item->category ?? '',
|
||||
'section' => $sectionLabels[$loop->index] ?? '',
|
||||
'title' => $titleLabels[$loop->index] ?? '',
|
||||
'owner' => $item->owner ?? '',
|
||||
'owner' => $item->owner_display ?? '',
|
||||
'assignee' => $item->assignee ?? '',
|
||||
'start_date' => $item->start_date?->format('Y-m-d') ?? '',
|
||||
'start_note' => $item->start_note ?? '',
|
||||
@ -599,7 +599,7 @@
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
@php($ownerNames = $personNames($item->owner))
|
||||
@php($ownerNames = $personNames($item->owner_display ?? null))
|
||||
<td class="sys-todo-table__people">
|
||||
@if ($ownerNames === [])
|
||||
—
|
||||
|
||||
@ -277,11 +277,140 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
'page' => 'systemization-todos',
|
||||
]))->assertOk()->viewData('content');
|
||||
|
||||
foreach (['todoOwners', 'todoAssignees'] as $key) {
|
||||
$this->assertContains('人参', $content[$key]);
|
||||
$this->assertContains('周生辰', $content[$key]);
|
||||
$this->assertNotContains('人参 / 周生辰', $content[$key]);
|
||||
}
|
||||
$this->assertSame(
|
||||
['人参', '晨曦', '常仪', '待定'],
|
||||
$content['todoOwners']->all(),
|
||||
);
|
||||
$this->assertContains('人参', $content['todoAssignees']);
|
||||
$this->assertContains('周生辰', $content['todoAssignees']);
|
||||
$this->assertNotContains('人参 / 周生辰', $content['todoAssignees']);
|
||||
$this->assertSame('人参', $content['items']->firstOrFail()->owner_display);
|
||||
}
|
||||
|
||||
public function test_owner_filter_uses_canonical_owner_mapping(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
$this->createTodo(
|
||||
'收入',
|
||||
'负责人映射',
|
||||
'玉兔归常仪',
|
||||
'2026-11-30',
|
||||
1,
|
||||
'玉兔',
|
||||
'玉兔',
|
||||
);
|
||||
$this->createTodo(
|
||||
'收入',
|
||||
'负责人映射',
|
||||
'周生辰归人参',
|
||||
'2026-11-30',
|
||||
2,
|
||||
'周生辰',
|
||||
'周生辰',
|
||||
);
|
||||
$this->createTodo(
|
||||
'收入',
|
||||
'负责人映射',
|
||||
'已有总负责人保持人参',
|
||||
'2026-11-30',
|
||||
3,
|
||||
'人参',
|
||||
'苏木',
|
||||
);
|
||||
$this->createTodo(
|
||||
'收入',
|
||||
'负责人映射',
|
||||
'待定负责人事项',
|
||||
'2026-11-30',
|
||||
4,
|
||||
'待定',
|
||||
'晨曦',
|
||||
);
|
||||
$this->createTodo(
|
||||
'收入',
|
||||
'负责人映射',
|
||||
'未填写负责人事项',
|
||||
'2026-11-30',
|
||||
5,
|
||||
'',
|
||||
'秋香',
|
||||
);
|
||||
|
||||
$changyiResponse = $this->get(route('business.pages.show', [
|
||||
'section' => 'other',
|
||||
'page' => 'systemization-todos',
|
||||
'owner' => ['常仪'],
|
||||
]))->assertOk();
|
||||
$changyiContent = $changyiResponse->viewData('content');
|
||||
$this->assertSame(
|
||||
['玉兔归常仪'],
|
||||
$changyiContent['items']->pluck('title')->all(),
|
||||
);
|
||||
$this->assertSame('常仪', $changyiContent['items']->firstOrFail()->owner_display);
|
||||
|
||||
$selfOwnerResponse = $this->get(route('business.pages.show', [
|
||||
'section' => 'other',
|
||||
'page' => 'systemization-todos',
|
||||
'owner' => ['人参'],
|
||||
]))->assertOk();
|
||||
$selfOwnerContent = $selfOwnerResponse->viewData('content');
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['周生辰归人参', '已有总负责人保持人参'],
|
||||
$selfOwnerContent['items']->pluck('title')->all(),
|
||||
);
|
||||
|
||||
$pendingResponse = $this->get(route('business.pages.show', [
|
||||
'section' => 'other',
|
||||
'page' => 'systemization-todos',
|
||||
'owner' => ['待定'],
|
||||
]))->assertOk();
|
||||
$this->assertEqualsCanonicalizing(
|
||||
['待定负责人事项', '未填写负责人事项'],
|
||||
$pendingResponse->viewData('content')['items']->pluck('title')->all(),
|
||||
);
|
||||
|
||||
$aliasFilterResponse = $this->get(route('business.pages.show', [
|
||||
'section' => 'other',
|
||||
'page' => 'systemization-todos',
|
||||
'owner' => ['玉兔'],
|
||||
]))->assertOk();
|
||||
$this->assertSame(
|
||||
['常仪'],
|
||||
$aliasFilterResponse->viewData('content')['todoFilters']['owners'],
|
||||
);
|
||||
}
|
||||
|
||||
public function test_manual_owner_values_are_saved_as_canonical_owners(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
|
||||
$this->post(route('business.systemization-todos.store'), [
|
||||
'category' => '收入',
|
||||
'section' => '负责人规范化',
|
||||
'title' => '新增时归并总负责人',
|
||||
'owner' => '苏木 / 李斯',
|
||||
'assignee' => '苏木 / 李斯',
|
||||
'start_date' => '2026-10-27',
|
||||
'due_date' => '2026-11-30',
|
||||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||||
])->assertRedirect();
|
||||
|
||||
$todo = SystemizationTodo::query()->firstOrFail();
|
||||
$this->assertSame('人参', $todo->owner);
|
||||
|
||||
$this->patch(route('business.systemization-todos.update', ['todo' => $todo->id]), [
|
||||
'category' => '收入',
|
||||
'section' => '负责人规范化',
|
||||
'title' => '新增时归并总负责人',
|
||||
'owner' => '玉兔',
|
||||
'assignee' => '玉兔',
|
||||
'start_date' => '2026-10-27',
|
||||
'due_date' => '2026-11-30',
|
||||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||||
])->assertRedirect();
|
||||
|
||||
$this->assertSame('常仪', $todo->refresh()->owner);
|
||||
}
|
||||
|
||||
public function test_can_insert_a_todo_at_a_selected_sequence_and_shift_existing_items(): void
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user