## 实际改动 - 系统化待办总负责人筛选统一为四类:人参、晨曦、常仪、待定。 - 苏木、李斯、周生辰归入人参;玉兔、山时、林冲、杳杳归入常仪;栎阳、秋香归入晨曦。 - 本身已是人参、晨曦、常仪、待定的数据保持不变。 - 历史非规范值在读取时归并,不批量改写现有数据;手工新增或修改时按四类保存。 - 表格显示、编辑弹窗和筛选查询使用同一套映射。 ## 验证证据 - SystemizationTodosTest 通过:21 tests,327 assertions。 - 覆盖玉兔归常仪、周生辰归人参、原有总负责人保留、待定及空值、旧筛选参数兼容。 - 不涉及数据库迁移、权限、金额口径或生产配置。 ## 合并前复查 - 基于最新 origin/main(4a1afd5246)开发。 - 已自审完整 diff,未包含敏感信息或无关修改。 - 提交:ba9fc15d。 Reviewed-on: #694
1338 lines
56 KiB
PHP
1338 lines
56 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use App\Business\SystemizationTodoWorkbookImporter;
|
||
use App\Models\Business\SystemizationTodo;
|
||
use App\Models\Business\SystemizationTodoAttachment;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Http\UploadedFile;
|
||
use Illuminate\Support\Facades\Storage;
|
||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
class SystemizationTodosTest extends FeatureTestCase
|
||
{
|
||
public function test_page_supports_daily_items_completion_progress_and_screenshots(): void
|
||
{
|
||
Storage::fake('uploads');
|
||
|
||
$pageResponse = $this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))
|
||
->assertOk()
|
||
->assertSeeText('财务系统化待办')
|
||
->assertSeeText('新增待办')
|
||
->assertSeeText('从 Excel 同步待办')
|
||
->assertSeeText('总负责人')
|
||
->assertSeeText('责任人')
|
||
->assertSeeText('待办事项序号')
|
||
->assertSeeText('已逾期')
|
||
->assertSeeText('开始时间')
|
||
->assertSeeText('完成时间')
|
||
->assertSeeText('待定')
|
||
->assertSee('name="reminder"', false)
|
||
->assertSeeText('全部提醒')
|
||
->assertSee('>查询</button>', false)
|
||
->assertSeeText('完成情况 / 备注')
|
||
->assertSeeText('提醒')
|
||
->assertSee('data-sys-todo-default-start-date', false)
|
||
->assertSee('data-sys-todo-history-open', false)
|
||
->assertSee('id="sysTodoHistoryDialog"', false)
|
||
->assertSee('data-sys-todo-history-export', false)
|
||
->assertSee('data-sys-todo-inline-add', false)
|
||
->assertSee('data-sys-todo-col-resizer', false)
|
||
->assertSee('data-sys-todo-reset-columns', false)
|
||
->assertSee('data-sys-todo-date-pending="start"', false)
|
||
->assertSee('data-sys-todo-date-pending="due"', false)
|
||
->assertSee('data-sys-todo-date-toggle="start"', false)
|
||
->assertSee('data-sys-todo-date-clear="due"', false)
|
||
->assertSee('data-date-picker', false)
|
||
->assertSee('window.CaiWuDatePicker', false)
|
||
->assertSee('placeholder="YYYY-MM-DD"', false)
|
||
->assertDontSee('id="sysTodoStartDate" type="date"', false)
|
||
->assertDontSee('id="sysTodoDueDate" type="date"', false)
|
||
->assertSee('name="start_pending"', false)
|
||
->assertSee('name="due_pending"', false)
|
||
->assertSee('data-sys-todo-owner-filter', false)
|
||
->assertSee('data-sys-todo-assignee-filter', false)
|
||
->assertSee('name="owner[]"', false)
|
||
->assertSee('name="assignee[]"', false)
|
||
->assertSee('data-sys-todo-category-select', false)
|
||
->assertSee('data-sys-todo-new-category-input', false)
|
||
->assertSee('data-sys-todo-section-select', false)
|
||
->assertSee('data-sys-todo-new-section-input', false)
|
||
->assertSee('data-sys-todo-position', false)
|
||
->assertSee('data-sys-todo-insert-after', 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)
|
||
->assertSeeText('+ 新增类型')
|
||
->assertSeeText('+ 新增内容')
|
||
->assertSeeText('在类型、内容下方的+直接新增')
|
||
->assertSee('data-sys-todo-choose-files', false)
|
||
->assertSee('data-sys-todo-remove-attachment', false)
|
||
->assertSee('data-sys-todo-remove-input', false)
|
||
->assertSee('is-marked-for-removal', false)
|
||
->assertSee('保存后删除', false)
|
||
->assertSee('data-sys-todo-open-delete', false)
|
||
->assertSee('id="sysTodoDeleteConfirm"', false)
|
||
->assertSee('data-sys-todo-table-wrap', false)
|
||
->assertSee('data-sys-todo-scrollrail', false)
|
||
->assertSee('data-sys-todo-image-prev', false)
|
||
->assertSee('data-sys-todo-image-next', false)
|
||
->assertDontSee('playlist_add', false)
|
||
->assertDontSee('<th scope="col">操作</th>', false)
|
||
->assertDontSeeText('下一页')
|
||
->assertSee('id="sysTodoImageViewer"', false);
|
||
$this->assertSame(0, substr_count((string) $pageResponse->getContent(), 'data-sys-todo-create'));
|
||
$pageData = $pageResponse->viewData('content');
|
||
$this->assertSame(
|
||
[SystemizationTodo::STATUS_INCOMPLETE, SystemizationTodo::STATUS_COMPLETED],
|
||
$pageData['todoStatuses'],
|
||
);
|
||
|
||
$storeResponse = $this->post(route('business.systemization-todos.store'), [
|
||
'category' => '收入',
|
||
'section' => '平台收入核对',
|
||
'title' => '核对淘系收入与平台账单差异',
|
||
'owner' => '晨曦、人参',
|
||
'assignee' => '栎阳',
|
||
'start_date' => '2026-10-25',
|
||
'due_date' => '2026-10-28',
|
||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||
'progress' => '已拉取账单,待核对订单',
|
||
'notes' => '周会跟进',
|
||
'screenshots' => [
|
||
$this->pngUpload('first.png'),
|
||
$this->pngUpload('existing.png'),
|
||
],
|
||
]);
|
||
$storeResponse->assertRedirect();
|
||
|
||
$todo = SystemizationTodo::query()->firstOrFail();
|
||
$location = (string) $storeResponse->headers->get('Location');
|
||
parse_str((string) parse_url($location, PHP_URL_QUERY), $redirectQuery);
|
||
$this->assertSame((string) $todo->id, $redirectQuery['highlight'] ?? null);
|
||
$this->assertSame('收入', $todo->category);
|
||
$this->assertSame('晨曦、人参', $todo->owner);
|
||
$this->assertSame(SystemizationTodo::STATUS_INCOMPLETE, $todo->status);
|
||
$this->assertSame(2, $todo->attachments()->count());
|
||
$this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))
|
||
->assertOk()
|
||
->assertSee('data-sys-todo-row-edit', false)
|
||
->assertSee('data-sys-todo-image-gallery', false)
|
||
->assertSeeText('截图 2');
|
||
$firstAttachment = $todo->attachments()->firstOrFail();
|
||
$existingAttachment = $todo->attachments()->reorder()->orderByDesc('id')->firstOrFail();
|
||
Storage::disk('uploads')->assertExists($firstAttachment->path);
|
||
Storage::disk('uploads')->assertExists($existingAttachment->path);
|
||
$this->get(route('business.systemization-todos.attachments.show', [
|
||
'todo' => $todo->id,
|
||
'attachment' => $firstAttachment->id,
|
||
]))->assertOk()->assertHeader('content-type', 'image/png');
|
||
|
||
$this->patch(route('business.systemization-todos.update', ['todo' => $todo->id]), [
|
||
'category' => '收入',
|
||
'section' => '平台收入核对',
|
||
'title' => '核对淘系收入与平台账单差异',
|
||
'owner' => '晨曦',
|
||
'assignee' => '栎阳',
|
||
'start_date' => '2026-10-25',
|
||
'due_date' => '2026-10-27',
|
||
'status' => SystemizationTodo::STATUS_COMPLETED,
|
||
'progress' => '已完成核对并留档',
|
||
'notes' => '周会确认关闭',
|
||
'remove_attachments' => [$firstAttachment->id],
|
||
'screenshots' => [$this->pngUpload('completed.png')],
|
||
])->assertRedirect();
|
||
|
||
$todo->refresh();
|
||
$this->assertSame(SystemizationTodo::STATUS_COMPLETED, $todo->status);
|
||
$this->assertSame('2026-10-27', $todo->completed_at?->format('Y-m-d'));
|
||
Storage::disk('uploads')->assertMissing($firstAttachment->path);
|
||
$this->assertTrue(
|
||
Storage::disk('uploads')->exists($existingAttachment->path),
|
||
'保留截图丢失:first=' . $firstAttachment->path . ' existing=' . $existingAttachment->path,
|
||
);
|
||
$this->assertSame(2, $todo->attachments()->count());
|
||
$this->assertTrue(
|
||
$todo->attachments()->pluck('original_name')->contains('completed.png')
|
||
);
|
||
|
||
$this->delete(route('business.systemization-todos.destroy', ['todo' => $todo->id]))
|
||
->assertRedirect();
|
||
$this->assertSoftDeleted('business_systemization_todos', ['id' => $todo->id]);
|
||
}
|
||
|
||
public function test_long_progress_and_notes_keep_the_standard_table_row_layout(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$progress = str_repeat(
|
||
'得物的安装费,是否在货款里;收款方式包括线下支付宝、京东、天猫、淘宝、拼多多及得物二维码。',
|
||
8,
|
||
);
|
||
$notes = str_repeat('已核对订单号 116212206747637437,货款流水与账单均已留档。', 5);
|
||
$this->createTodo(
|
||
'费用',
|
||
'平台费用核对',
|
||
'核对安装费与货款',
|
||
'2026-10-30',
|
||
1,
|
||
)->update([
|
||
'progress' => $progress,
|
||
'notes' => $notes,
|
||
]);
|
||
|
||
$response = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk();
|
||
$html = (string) $response->getContent();
|
||
|
||
$response
|
||
->assertSee('<div class="sys-todo-table__progress-notes-content">', false)
|
||
->assertSee('<span class="sys-todo-table__detail-text">' . e($progress) . '</span>', false)
|
||
->assertSee('<span class="sys-todo-table__detail-text">' . e($notes) . '</span>', false);
|
||
$this->assertStringNotContainsString('.sys-todo-table__progress-notes { display:grid;', $html);
|
||
$this->assertStringNotContainsString('.sys-todo-table td { height:3.5rem;', $html);
|
||
$this->assertStringNotContainsString('.sys-todo-table tbody tr { height:3.5rem; }', $html);
|
||
$this->assertMatchesRegularExpression(
|
||
'/<td class="sys-todo-table__progress-notes">\s*<div class="sys-todo-table__progress-notes-content">.*?'
|
||
. preg_quote(e($progress), '/')
|
||
. '.*?' . preg_quote(e($notes), '/')
|
||
. '.*?<\/div>\s*<\/td>/s',
|
||
$html,
|
||
);
|
||
}
|
||
|
||
public function test_multi_person_owners_and_assignees_render_one_name_per_line(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$this->createTodo(
|
||
'收入',
|
||
'多人责任人展示',
|
||
'检查多人姓名换行',
|
||
'2026-11-30',
|
||
1,
|
||
'晨曦、人参',
|
||
'人参、秋香、常仪',
|
||
);
|
||
|
||
$response = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk();
|
||
$html = (string) $response->getContent();
|
||
|
||
$this->assertStringContainsString(
|
||
'<span class="sys-todo-table__people-list">',
|
||
$html,
|
||
);
|
||
$this->assertMatchesRegularExpression(
|
||
'/<td class="sys-todo-table__people">\s*<span class="sys-todo-table__people-list">\s*'
|
||
. '<span class="sys-todo-table__person">晨曦<\/span>\s*'
|
||
. '<span class="sys-todo-table__person">人参<\/span>\s*'
|
||
. '<\/span>\s*<\/td>/s',
|
||
$html,
|
||
);
|
||
$this->assertMatchesRegularExpression(
|
||
'/<td class="sys-todo-table__people">\s*<span class="sys-todo-table__people-list">\s*'
|
||
. '<span class="sys-todo-table__person">人参<\/span>\s*'
|
||
. '<span class="sys-todo-table__person">秋香<\/span>\s*'
|
||
. '<span class="sys-todo-table__person">常仪<\/span>\s*'
|
||
. '<\/span>\s*<\/td>/s',
|
||
$html,
|
||
);
|
||
}
|
||
|
||
public function test_people_filter_splits_slash_separated_names(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$this->createTodo(
|
||
'收入',
|
||
'筛选责任人拆分',
|
||
'核对斜杠分隔的多人姓名',
|
||
'2026-11-30',
|
||
1,
|
||
'人参 / 周生辰',
|
||
'人参 / 周生辰',
|
||
);
|
||
|
||
$content = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk()->viewData('content');
|
||
|
||
$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
|
||
{
|
||
$this->loginAs();
|
||
|
||
$items = [];
|
||
foreach (range(1, 7) as $index) {
|
||
$items[$index] = $this->createTodo(
|
||
'收入',
|
||
'平台收入核对',
|
||
'原待办 ' . $index,
|
||
'2026-11-30',
|
||
$index,
|
||
);
|
||
}
|
||
|
||
$initialPage = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk()
|
||
->assertSee('title="在第 3 条后新增一条"', false)
|
||
->assertSee('data-sys-todo-insert-position="4"', false);
|
||
$this->assertMatchesRegularExpression(
|
||
'/id="sys-todo-row-' . $items[3]->id . '".*?data-sys-todo-insert-position="4"/s',
|
||
(string) $initialPage->getContent(),
|
||
);
|
||
|
||
$this->post(route('business.systemization-todos.store'), [
|
||
'category' => '收入',
|
||
'section' => '平台收入核对',
|
||
'position' => 7,
|
||
'title' => '新增待办 7',
|
||
'owner' => '晨曦',
|
||
'assignee' => '栎阳',
|
||
'status' => SystemizationTodo::STATUS_PENDING,
|
||
])->assertRedirect()
|
||
->assertSessionHas('success', '待办事项已新增为当前内容第 7 条并定位。');
|
||
|
||
$page = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk()->viewData('content');
|
||
$group = $page['items']
|
||
->where('category', '收入')
|
||
->where('section', '平台收入核对')
|
||
->values();
|
||
|
||
$this->assertSame([
|
||
'原待办 1',
|
||
'原待办 2',
|
||
'原待办 3',
|
||
'原待办 4',
|
||
'原待办 5',
|
||
'原待办 6',
|
||
'新增待办 7',
|
||
'原待办 7',
|
||
], $group->pluck('title')->all());
|
||
$this->assertSame(
|
||
range(1, 8),
|
||
$group->pluck('item_sequence')->map(static fn (mixed $value): int => (int) $value)->all(),
|
||
);
|
||
$this->assertGreaterThan(
|
||
$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_can_add_a_new_sequence_after_the_last_todo(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
foreach (range(1, 7) as $index) {
|
||
$this->createTodo(
|
||
'收入',
|
||
'平台收入核对',
|
||
'原待办 ' . $index,
|
||
'2026-11-30',
|
||
$index,
|
||
);
|
||
}
|
||
|
||
$this->post(route('business.systemization-todos.store'), [
|
||
'category' => '收入',
|
||
'section' => '平台收入核对',
|
||
'position' => 8,
|
||
'title' => '新增待办 8',
|
||
'owner' => '晨曦',
|
||
'assignee' => '栎阳',
|
||
'status' => SystemizationTodo::STATUS_PENDING,
|
||
])->assertRedirect()
|
||
->assertSessionHas('success', '待办事项已新增为当前内容第 8 条并定位。');
|
||
|
||
$group = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk()->viewData('content')['items']
|
||
->where('category', '收入')
|
||
->where('section', '平台收入核对')
|
||
->values();
|
||
|
||
$this->assertSame([
|
||
'原待办 1',
|
||
'原待办 2',
|
||
'原待办 3',
|
||
'原待办 4',
|
||
'原待办 5',
|
||
'原待办 6',
|
||
'原待办 7',
|
||
'新增待办 8',
|
||
], $group->pluck('title')->all());
|
||
$this->assertSame(8, (int) $group->last()->item_sequence);
|
||
}
|
||
|
||
public function test_type_and_content_cells_can_be_edited_for_the_whole_group(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$incomeA = $this->createTodo('收入', '平台收入核对', '收入事项 A', '2026-11-30', 1);
|
||
$incomeB = $this->createTodo('收入', '平台收入核对', '收入事项 B', '2026-11-30', 2);
|
||
$incomeOther = $this->createTodo('收入', '退款核对', '退款事项', '2026-11-30', 3);
|
||
$cost = $this->createTodo('成本', '成本核对', '成本事项', '2026-11-30', 4);
|
||
|
||
$this->patch(route('business.systemization-todos.category', ['todo' => $incomeA->id]), [
|
||
'category' => '业务收入',
|
||
])->assertRedirect();
|
||
|
||
foreach ([$incomeA, $incomeB, $incomeOther] as $todo) {
|
||
$this->assertSame('业务收入', $todo->fresh()->category);
|
||
}
|
||
$this->assertSame('成本', $cost->fresh()->category);
|
||
|
||
$this->patch(route('business.systemization-todos.section', ['todo' => $incomeA->id]), [
|
||
'section' => '收入核对新版',
|
||
])->assertRedirect();
|
||
|
||
$this->assertSame('收入核对新版', $incomeA->fresh()->section);
|
||
$this->assertSame('收入核对新版', $incomeB->fresh()->section);
|
||
$this->assertSame('退款核对', $incomeOther->fresh()->section);
|
||
$this->assertSame('成本核对', $cost->fresh()->section);
|
||
}
|
||
|
||
public function test_excel_import_preserves_hierarchy_and_does_not_overwrite_existing_rows(): void
|
||
{
|
||
$path = sys_get_temp_dir() . '/systemization-todos-' . uniqid() . '.xlsx';
|
||
$spreadsheet = new Spreadsheet();
|
||
$sheet = $spreadsheet->getActiveSheet();
|
||
$sheet->fromArray([
|
||
['类型', '内容', '待办事项(具体工作计划)', '总负责人', '责任人', '开始时间', '完成时间', '完成情况(放截图)', '备注'],
|
||
['收入', '平台收入核对', '核对淘系账单', '晨曦', '栎阳', '2026-10-20', '2026-10-21', '已完成', '首轮导入'],
|
||
[null, null, '核对拼多多账单', '晨曦', '秋香', '待定', '11月', '第一步已开始', null],
|
||
]);
|
||
(new Xlsx($spreadsheet))->save($path);
|
||
$spreadsheet->disconnectWorksheets();
|
||
|
||
try {
|
||
$this->loginAs()->post(route('business.systemization-todos.import'), [
|
||
'source' => new UploadedFile(
|
||
$path,
|
||
'10.19工作梳理.xlsx',
|
||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
null,
|
||
true,
|
||
),
|
||
])->assertRedirect();
|
||
|
||
$this->assertSame(2, SystemizationTodo::query()->count());
|
||
$first = SystemizationTodo::query()->orderBy('sort_order')->firstOrFail();
|
||
$second = SystemizationTodo::query()->orderByDesc('sort_order')->firstOrFail();
|
||
$this->assertSame('收入', $first->category);
|
||
$this->assertSame('平台收入核对', $first->section);
|
||
$this->assertSame(SystemizationTodo::STATUS_COMPLETED, $first->status);
|
||
$this->assertSame('收入', $second->category);
|
||
$this->assertSame('平台收入核对', $second->section);
|
||
$this->assertSame('待定', $second->start_note);
|
||
$this->assertSame('11月', $second->due_note);
|
||
$this->assertSame(SystemizationTodo::STATUS_IN_PROGRESS, $second->status);
|
||
|
||
$result = app(SystemizationTodoWorkbookImporter::class)
|
||
->import($path, (int) company_id(), user_id());
|
||
$this->assertSame(0, $result['created']);
|
||
$this->assertSame(2, $result['skipped']);
|
||
|
||
$this->assertSame(2, SystemizationTodo::query()->count());
|
||
$this->assertSame(0, SystemizationTodoAttachment::query()->count());
|
||
} finally {
|
||
@unlink($path);
|
||
}
|
||
}
|
||
|
||
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_pending' => 1,
|
||
'due_date' => null,
|
||
'due_pending' => 1,
|
||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||
'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_pending' => 1,
|
||
'due_date' => null,
|
||
'due_note' => '11月',
|
||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||
'progress' => '等待平台确认',
|
||
'notes' => '',
|
||
])->assertRedirect();
|
||
|
||
$todo->refresh();
|
||
$this->assertSame('待确认开始与完成日期(已更新)', $todo->title);
|
||
$this->assertSame('待定', $todo->start_note);
|
||
$this->assertSame('11月', $todo->due_note);
|
||
}
|
||
|
||
public function test_edit_redirect_keeps_current_filters_and_highlight(): void
|
||
{
|
||
$this->loginAs();
|
||
$todo = $this->createTodo('收入', '平台收入核对', '筛选保持测试', '2026-10-30', 1);
|
||
$returnFilters = base64_encode(json_encode([
|
||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||
'reminder' => 'upcoming',
|
||
'category' => '收入',
|
||
'owners' => ['晨曦', '人参'],
|
||
'assignees' => ['栎阳'],
|
||
'start_date' => '2026-10-25',
|
||
'due_date' => '2026-10-30',
|
||
'keyword' => '筛选',
|
||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
||
|
||
$response = $this->patch(route('business.systemization-todos.update', ['todo' => $todo->id]), [
|
||
'category' => '收入',
|
||
'section' => '平台收入核对',
|
||
'title' => '筛选保持测试(已更新)',
|
||
'owner' => '晨曦',
|
||
'assignee' => '栎阳',
|
||
'start_date' => '2026-10-25',
|
||
'due_date' => '2026-10-30',
|
||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||
'return_filters' => $returnFilters,
|
||
])->assertRedirect();
|
||
|
||
$location = (string) $response->headers->get('Location');
|
||
parse_str((string) parse_url($location, PHP_URL_QUERY), $query);
|
||
$this->assertSame(SystemizationTodo::STATUS_INCOMPLETE, $query['status'] ?? null);
|
||
$this->assertSame('upcoming', $query['reminder'] ?? null);
|
||
$this->assertSame('收入', $query['category'] ?? null);
|
||
$this->assertSame(['晨曦', '人参'], $query['owner'] ?? null);
|
||
$this->assertSame(['栎阳'], $query['assignee'] ?? null);
|
||
$this->assertSame('2026-10-25', $query['start_date'] ?? null);
|
||
$this->assertSame('2026-10-30', $query['due_date'] ?? null);
|
||
$this->assertSame('筛选', $query['keyword'] ?? null);
|
||
$this->assertSame((string) $todo->id, $query['highlight'] ?? null);
|
||
}
|
||
|
||
public function test_reminder_and_status_treat_legacy_open_states_as_incomplete(): void
|
||
{
|
||
Carbon::setTestNow('2026-10-26 12:00:00');
|
||
try {
|
||
$this->loginAs();
|
||
$this->createTodo('收入', '提醒测试', '逾期事项', '2026-10-20', 1);
|
||
$this->createTodo('收入', '提醒测试', '今天到期事项', '2026-10-26', 2);
|
||
$this->createTodo('收入', '提醒测试', '未来事项', '2026-10-30', 3);
|
||
$completed = $this->createTodo('收入', '提醒测试', '已完成事项', '2026-10-22', 4);
|
||
$completed->update([
|
||
'status' => SystemizationTodo::STATUS_COMPLETED,
|
||
'completed_at' => '2026-10-22',
|
||
'progress' => '已完成',
|
||
]);
|
||
$legacy = $this->createTodo('收入', '提醒测试', '历史进行中事项', '2026-11-01', 5);
|
||
$legacy->update(['status' => '进行中']);
|
||
$pendingDate = $this->createTodo('收入', '提醒测试', '时间待定事项', '2026-10-30', 6);
|
||
$pendingDate->update(['due_date' => null, 'due_note' => '待定']);
|
||
|
||
$pageResponse = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk()
|
||
->assertSee('data-sys-todo-default-start-date="2026-10-26"', false)
|
||
->assertSee("payload.start_date || (!currentTodoId ? defaultStartDate : '')", false)
|
||
->assertSee("applyDateControl(startDateControl, defaultStartDate, '');", false);
|
||
$content = $pageResponse->viewData('content');
|
||
$items = $content['items']->keyBy('title');
|
||
|
||
$this->assertSame('逾期 6 天', $items['逾期事项']->reminder_label);
|
||
$this->assertSame('danger', $items['逾期事项']->reminder_tone);
|
||
$this->assertSame('今天到期', $items['今天到期事项']->reminder_label);
|
||
$this->assertSame('剩余 4 天', $items['未来事项']->reminder_label);
|
||
$this->assertSame('—', $items['已完成事项']->reminder_label);
|
||
$this->assertSame('时间待定', $items['时间待定事项']->reminder_label);
|
||
$this->assertSame('', $items['已完成事项']->displayProgress());
|
||
$this->assertSame(SystemizationTodo::STATUS_INCOMPLETE, $items['历史进行中事项']->status_label);
|
||
|
||
$incompleteTitles = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||
]))->assertOk()->viewData('content')['items']->pluck('title');
|
||
$this->assertContains('历史进行中事项', $incompleteTitles);
|
||
$this->assertNotContains('已完成事项', $incompleteTitles);
|
||
|
||
$completedTitles = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
'status' => SystemizationTodo::STATUS_COMPLETED,
|
||
]))->assertOk()->viewData('content')['items']->pluck('title');
|
||
$this->assertSame(['已完成事项'], $completedTitles->all());
|
||
|
||
foreach ([
|
||
'overdue' => ['逾期事项'],
|
||
'today' => ['今天到期事项'],
|
||
'upcoming' => ['未来事项', '历史进行中事项'],
|
||
'pending' => ['时间待定事项'],
|
||
'none' => ['已完成事项'],
|
||
] as $reminder => $expectedTitles) {
|
||
$reminderContent = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
'reminder' => $reminder,
|
||
]))->assertOk()->viewData('content');
|
||
|
||
$this->assertSame($reminder, $reminderContent['todoFilters']['reminder']);
|
||
$this->assertSame($expectedTitles, $reminderContent['items']->pluck('title')->all());
|
||
}
|
||
} finally {
|
||
Carbon::setTestNow();
|
||
}
|
||
}
|
||
|
||
public function test_page_filters_by_date_and_merges_adjacent_category_and_section_cells(): void
|
||
{
|
||
$this->travelTo(Carbon::parse('2026-10-26 12:00:00'));
|
||
$this->loginAs();
|
||
|
||
$this->createTodo('1、收入', '1、平台收入核对', '1、收入事项 A', '2026-10-26', 1, '晨曦', '栎阳、秋香');
|
||
$this->createTodo('收入', '平台收入核对', '2、收入事项 B', '2026-10-27', 2);
|
||
$this->createTodo('成本', '3、退款核销', '3、成本事项', '2026-10-26', 3, '常仪', '山时');
|
||
$this->createTodo('费用', '4、逾期核对', '4、逾期事项', '2026-10-20', 4, '人参', '玉兔');
|
||
|
||
$pageResponse = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]));
|
||
$pageResponse
|
||
->assertOk()
|
||
->assertSee('rowspan="2"', false)
|
||
->assertSee('data-sys-todo-column="section"', false)
|
||
->assertSee('data-sys-todo-column="sequence"', false)
|
||
->assertSee('data-sys-todo-column="title"', false)
|
||
->assertSee('sys-todo-table__type', false)
|
||
->assertSee('title="类型序号">1</span>', false)
|
||
->assertSee('title="内容序号">1</span>', false)
|
||
->assertSee('title="当前内容内待办序号">2</span>', false)
|
||
->assertDontSee('1、平台收入核对')
|
||
->assertDontSee('3、退款核销')
|
||
->assertDontSee('1、收入事项 A')
|
||
->assertDontSee('3、成本事项')
|
||
->assertSee('收入事项 A')
|
||
->assertSee('收入事项 B');
|
||
$pageContent = $pageResponse->viewData('content');
|
||
$this->assertContains('平台收入核对', $pageContent['todoContentOptions']['收入']);
|
||
$this->assertArrayNotHasKey('1、收入', $pageContent['todoContentOptions']);
|
||
$this->assertSame(
|
||
[SystemizationTodo::STATUS_INCOMPLETE, SystemizationTodo::STATUS_COMPLETED],
|
||
array_column($pageContent['todoStatusOptions']->all(), 'value'),
|
||
);
|
||
$pageItems = $pageContent['items'];
|
||
$this->assertSame(
|
||
[1, 1],
|
||
$pageItems->where('category_display', '收入')
|
||
->pluck('type_sequence')
|
||
->map(static fn (mixed $value): int => (int) $value)
|
||
->values()
|
||
->all(),
|
||
);
|
||
$this->assertSame(
|
||
[1, 2],
|
||
$pageItems->where('category_display', '收入')
|
||
->pluck('item_sequence')
|
||
->map(static fn (mixed $value): int => (int) $value)
|
||
->values()
|
||
->all(),
|
||
);
|
||
$this->assertSame(
|
||
[2],
|
||
$pageItems->where('category', '成本')
|
||
->pluck('type_sequence')
|
||
->map(static fn (mixed $value): int => (int) $value)
|
||
->values()
|
||
->all(),
|
||
);
|
||
|
||
$highlightedTodo = $pageItems->firstWhere('title', '2、收入事项 B');
|
||
$this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
'highlight' => $highlightedTodo->id,
|
||
]))
|
||
->assertOk()
|
||
->assertSee('id="sys-todo-row-' . $highlightedTodo->id . '" class="is-highlighted"', false);
|
||
|
||
$response = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
'due_date' => '2026-10-26',
|
||
]));
|
||
|
||
$response->assertOk()->assertSee('收入事项 A')->assertSee('成本事项');
|
||
$content = $response->viewData('content');
|
||
$this->assertNotContains('收入事项 B', $content['items']->pluck('title')->all());
|
||
$this->assertSame([
|
||
'due_date' => '2026-10-26',
|
||
], array_intersect_key($content['todoFilters'], array_flip(['due_date'])));
|
||
|
||
$peopleResponse = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
'assignee' => ['秋香', '山时'],
|
||
]))->assertOk();
|
||
$peopleContent = $peopleResponse->viewData('content');
|
||
$this->assertEqualsCanonicalizing(
|
||
['1、收入事项 A', '3、成本事项'],
|
||
$peopleContent['items']->pluck('title')->all(),
|
||
);
|
||
|
||
$ownerResponse = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
'owner' => ['常仪', '人参'],
|
||
]))->assertOk();
|
||
$this->assertEqualsCanonicalizing(
|
||
['3、成本事项', '4、逾期事项'],
|
||
$ownerResponse->viewData('content')['items']->pluck('title')->all(),
|
||
);
|
||
|
||
$overdueResponse = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
'status' => '__overdue__',
|
||
]))->assertOk();
|
||
$this->assertSame(
|
||
['4、逾期事项'],
|
||
$overdueResponse->viewData('content')['items']->pluck('title')->all(),
|
||
);
|
||
}
|
||
|
||
public function test_same_content_stays_merged_and_receives_scoped_hierarchy_sequences(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$mergedSection = '钉钉流程,纳税人 3、盘点';
|
||
$mergedSectionVariant = '钉钉流程,纳税人3、盘点';
|
||
$pending = $this->createTodo('收入', $mergedSection, '待办事项 A', '2026-10-30', 1);
|
||
$completed = $this->createTodo('收入', $mergedSectionVariant, '待办事项 B', '2026-10-10', 2);
|
||
$completed->update([
|
||
'status' => SystemizationTodo::STATUS_COMPLETED,
|
||
'completed_at' => '2026-10-10',
|
||
]);
|
||
$inProgress = $this->createTodo('收入', $mergedSection, '待办事项 C', '2026-10-20', 3);
|
||
$inProgress->update(['status' => SystemizationTodo::STATUS_IN_PROGRESS]);
|
||
$secondContent = $this->createTodo('收入', '报销流程', '待办事项 D', '2026-11-01', 4);
|
||
$cost = $this->createTodo('成本', '成本核对', '待办事项 E', '2026-11-02', 5);
|
||
|
||
$response = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk();
|
||
$content = $response->viewData('content');
|
||
$items = $content['items'];
|
||
|
||
$this->assertSame(
|
||
[$pending->id, $completed->id, $inProgress->id, $secondContent->id, $cost->id],
|
||
$items->pluck('id')->all(),
|
||
);
|
||
$this->assertSame(
|
||
[[1, 1, [1, 2, 3]], [1, 2, [1]], [2, 1, [1]]],
|
||
$items
|
||
->groupBy(static fn (SystemizationTodo $todo): string => $todo->type_sequence . ':' . $todo->content_sequence)
|
||
->map(static fn ($group): array => [
|
||
(int) $group->first()->type_sequence,
|
||
(int) $group->first()->content_sequence,
|
||
$group->pluck('item_sequence')->map(static fn (mixed $value): int => (int) $value)->all(),
|
||
])
|
||
->values()
|
||
->all(),
|
||
);
|
||
$this->assertSame(
|
||
[$pending->id, $completed->id, $inProgress->id],
|
||
$items
|
||
->where('type_sequence', 1)
|
||
->where('content_sequence', 1)
|
||
->whereIn('status', [SystemizationTodo::STATUS_COMPLETED, SystemizationTodo::STATUS_IN_PROGRESS])
|
||
->pluck('id')
|
||
->all(),
|
||
);
|
||
$this->assertSame(
|
||
1,
|
||
substr_count(
|
||
(string) $response->getContent(),
|
||
'sys-todo-table__group-text">' . $mergedSection . '</span>',
|
||
),
|
||
);
|
||
}
|
||
|
||
public function test_page_renders_all_todos_on_one_continuous_list(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
foreach (range(1, 55) as $index) {
|
||
$this->createTodo(
|
||
$index % 2 === 0 ? '收入' : '成本',
|
||
'连续列表测试',
|
||
'连续事项 ' . $index,
|
||
'2026-11-30',
|
||
$index,
|
||
);
|
||
}
|
||
|
||
$response = $this->get(route('business.pages.show', [
|
||
'section' => 'other',
|
||
'page' => 'systemization-todos',
|
||
]))->assertOk();
|
||
|
||
$content = $response->viewData('content');
|
||
$this->assertCount(55, $content['items']);
|
||
$this->assertTrue($content['items']->pluck('title')->contains('连续事项 55'));
|
||
$response->assertSee('连续事项 55')
|
||
->assertSeeText('共 55 条,已全部显示')
|
||
->assertDontSeeText('下一页')
|
||
->assertDontSeeText('上一页');
|
||
}
|
||
|
||
public function test_excel_sync_updates_changed_rows_and_preserves_attachments(): void
|
||
{
|
||
Storage::fake('uploads');
|
||
$this->loginAs();
|
||
|
||
$initialPath = sys_get_temp_dir() . '/systemization-sync-initial-' . uniqid() . '.xlsx';
|
||
$updatedPath = sys_get_temp_dir() . '/systemization-sync-updated-' . uniqid() . '.xlsx';
|
||
|
||
try {
|
||
$this->writeWorkbook($initialPath, [
|
||
['收入', '顾家、索菲亚过账', '系统搭建', '常仪', '晨曦', '2026-11-20', '2026-11-22', null, '原备注'],
|
||
]);
|
||
app(SystemizationTodoWorkbookImporter::class)
|
||
->import($initialPath, (int) company_id(), user_id());
|
||
|
||
$todo = SystemizationTodo::query()->firstOrFail();
|
||
Storage::disk('uploads')->put('systemization-todos/test.png', 'image');
|
||
SystemizationTodoAttachment::query()->create([
|
||
'company_id' => (int) company_id(),
|
||
'systemization_todo_id' => $todo->id,
|
||
'path' => 'systemization-todos/test.png',
|
||
'original_name' => 'existing.png',
|
||
'mime_type' => 'image/png',
|
||
'size' => 5,
|
||
'uploaded_by' => user_id(),
|
||
]);
|
||
|
||
$this->writeWorkbook($updatedPath, [
|
||
['收入', '顾家、索菲亚过账', '系统搭建,系统搭建好之后交接给林冲', '晨曦', '蕙兰', '2026-11-23', '2026-11-25', '转接', null],
|
||
[null, null, '新增系统交接说明', '晨曦', '蕙兰', '2026-10-26', '2026-10-27', null, null],
|
||
]);
|
||
|
||
$preview = app(SystemizationTodoWorkbookImporter::class)
|
||
->sync($updatedPath, (int) company_id(), user_id(), true);
|
||
$this->assertSame(1, $preview['created']);
|
||
$this->assertSame(1, $preview['updated']);
|
||
$this->assertSame(0, $preview['retained']);
|
||
$this->assertSame(1, SystemizationTodo::query()->count());
|
||
|
||
$result = app(SystemizationTodoWorkbookImporter::class)
|
||
->sync($updatedPath, (int) company_id(), user_id());
|
||
$this->assertSame(1, $result['created']);
|
||
$this->assertSame(1, $result['updated']);
|
||
$this->assertSame(0, $result['retained']);
|
||
|
||
$todo->refresh();
|
||
$this->assertSame('系统搭建,系统搭建好之后交接给林冲', $todo->title);
|
||
$this->assertSame('蕙兰', $todo->assignee);
|
||
$this->assertSame('2026-11-23', $todo->start_date?->format('Y-m-d'));
|
||
$this->assertSame('2026-11-25', $todo->due_date?->format('Y-m-d'));
|
||
$this->assertSame(SystemizationTodo::STATUS_IN_PROGRESS, $todo->status);
|
||
$this->assertSame(1, $todo->attachments()->count());
|
||
$this->assertTrue(SystemizationTodo::query()->where('title', '新增系统交接说明')->exists());
|
||
|
||
$secondResult = app(SystemizationTodoWorkbookImporter::class)
|
||
->sync($updatedPath, (int) company_id(), user_id());
|
||
$this->assertSame(0, $secondResult['created']);
|
||
$this->assertSame(0, $secondResult['updated']);
|
||
$this->assertSame(2, SystemizationTodo::query()->count());
|
||
} finally {
|
||
@unlink($initialPath);
|
||
@unlink($updatedPath);
|
||
}
|
||
}
|
||
|
||
public function test_uploaded_workbook_uses_original_filename_when_temp_path_has_no_extension(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$sourcePath = sys_get_temp_dir() . '/systemization-upload-source-' . uniqid() . '.xlsx';
|
||
$uploadPath = sys_get_temp_dir() . '/systemization-upload-' . uniqid();
|
||
try {
|
||
$this->writeWorkbook($sourcePath, [[
|
||
'收入',
|
||
'上传测试',
|
||
'浏览器上传的工作簿',
|
||
'晨曦',
|
||
'栎阳',
|
||
'2026-10-27',
|
||
'2026-10-30',
|
||
null,
|
||
null,
|
||
]]);
|
||
copy($sourcePath, $uploadPath);
|
||
|
||
$upload = new UploadedFile(
|
||
$uploadPath,
|
||
'10.19工作梳理.xlsx',
|
||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
null,
|
||
true,
|
||
);
|
||
|
||
$this->post(route('business.systemization-todos.import'), [
|
||
'source' => $upload,
|
||
])->assertRedirect()
|
||
->assertSessionHas('success');
|
||
|
||
$this->assertDatabaseHas('business_systemization_todos', [
|
||
'company_id' => (int) company_id(),
|
||
'title' => '浏览器上传的工作簿',
|
||
]);
|
||
$this->assertDatabaseHas('business_systemization_todo_audit_logs', [
|
||
'company_id' => (int) company_id(),
|
||
'action' => 'import',
|
||
]);
|
||
} finally {
|
||
@unlink($sourcePath);
|
||
@unlink($uploadPath);
|
||
}
|
||
}
|
||
|
||
public function test_manual_changes_are_listed_in_operation_history_and_csv(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$this->post(route('business.systemization-todos.store'), [
|
||
'category' => '收入',
|
||
'section' => '操作记录测试',
|
||
'title' => '原待办',
|
||
'owner' => '晨曦',
|
||
'assignee' => '栎阳',
|
||
'start_date' => '2026-10-27',
|
||
'due_date' => '2026-10-30',
|
||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||
])->assertRedirect();
|
||
|
||
$todo = SystemizationTodo::query()->firstOrFail();
|
||
$this->patch(route('business.systemization-todos.update', ['todo' => $todo->id]), [
|
||
'category' => '收入',
|
||
'section' => '操作记录测试',
|
||
'title' => '修改后的待办',
|
||
'owner' => '晨曦、人参',
|
||
'assignee' => '栎阳',
|
||
'start_date' => '2026-10-27',
|
||
'due_date' => '2026-10-31',
|
||
'status' => SystemizationTodo::STATUS_COMPLETED,
|
||
'progress' => '已完成并留档',
|
||
])->assertRedirect();
|
||
|
||
$this->delete(route('business.systemization-todos.destroy', ['todo' => $todo->id]))
|
||
->assertRedirect();
|
||
|
||
$history = $this->getJson(route('business.systemization-todos.history'))
|
||
->assertOk()
|
||
->json();
|
||
|
||
$this->assertSame(['delete', 'update', 'create'], array_column($history['events'], 'action'));
|
||
$this->assertSame($this->user->name, $history['events'][0]['actor']);
|
||
$this->assertSame('修改后的待办', $history['events'][1]['title']);
|
||
$this->assertContains('待办事项:原待办 → 修改后的待办', $history['events'][1]['changes']);
|
||
$this->assertContains('状态:未完成 → 已完成', $history['events'][1]['changes']);
|
||
$this->assertStringContainsString('删除前:收入 · 操作记录测试 · 修改后的待办', $history['events'][0]['changes'][0]);
|
||
|
||
$csv = $this->get(
|
||
route('business.systemization-todos.history.export') . '?action=update'
|
||
)->streamedContent();
|
||
$this->assertStringContainsString('时间,操作,类型,内容,待办事项,变更内容,操作人', $csv);
|
||
$this->assertStringContainsString('修改,收入,操作记录测试,修改后的待办', $csv);
|
||
$this->assertStringNotContainsString('删除前:', $csv);
|
||
$this->assertStringNotContainsString('新增:', $csv);
|
||
}
|
||
|
||
public function test_excel_sync_audits_changes_and_matches_numbered_group_labels_without_duplicates(): void
|
||
{
|
||
$this->loginAs();
|
||
|
||
$todo = $this->createTodo(
|
||
'收入',
|
||
'平台收入核对',
|
||
'平台收入核对',
|
||
'2026-11-30',
|
||
10,
|
||
);
|
||
$todo->update(['imported_at' => now()]);
|
||
|
||
$path = sys_get_temp_dir() . '/systemization-numbered-sync-' . uniqid() . '.xlsx';
|
||
try {
|
||
$this->writeWorkbook($path, [[
|
||
'1、收入',
|
||
'2、平台收入核对',
|
||
'1、平台收入核对(已更新)',
|
||
'晨曦',
|
||
'栎阳',
|
||
'2026-10-27',
|
||
'2026-11-30',
|
||
null,
|
||
null,
|
||
]]);
|
||
|
||
$preview = app(SystemizationTodoWorkbookImporter::class)
|
||
->sync($path, (int) company_id(), user_id(), true);
|
||
$this->assertSame(0, $preview['created']);
|
||
$this->assertSame(1, $preview['updated']);
|
||
$this->assertSame(0, $preview['retained']);
|
||
$this->assertDatabaseCount('business_systemization_todo_audit_logs', 0);
|
||
|
||
$result = app(SystemizationTodoWorkbookImporter::class)
|
||
->sync($path, (int) company_id(), user_id());
|
||
$this->assertSame(0, $result['created']);
|
||
$this->assertSame(1, $result['updated']);
|
||
$this->assertSame(0, $result['retained']);
|
||
$this->assertSame(1, SystemizationTodo::query()->count());
|
||
|
||
$todo->refresh();
|
||
$this->assertSame('收入', $todo->category);
|
||
$this->assertSame('平台收入核对', $todo->section);
|
||
$this->assertSame('平台收入核对(已更新)', $todo->title);
|
||
$this->assertDatabaseHas('business_systemization_todo_audit_logs', [
|
||
'company_id' => (int) company_id(),
|
||
'todo_id' => $todo->id,
|
||
'action' => 'import',
|
||
]);
|
||
|
||
$event = $this->getJson(route('business.systemization-todos.history'))
|
||
->assertOk()
|
||
->json('events.0');
|
||
$this->assertSame('import', $event['action']);
|
||
$this->assertContains(
|
||
'待办事项:平台收入核对 → 平台收入核对(已更新)',
|
||
$event['changes']
|
||
);
|
||
} finally {
|
||
@unlink($path);
|
||
}
|
||
}
|
||
|
||
public function test_excel_sync_splits_numbered_titles_and_keeps_attachment_on_first_item(): void
|
||
{
|
||
Storage::fake('uploads');
|
||
$this->loginAs();
|
||
|
||
$combinedTitle = "1、核对平台收入\n\n2、核对订单收入\n3,核对特殊订单";
|
||
$historical = SystemizationTodo::query()->create([
|
||
'company_id' => (int) company_id(),
|
||
'category' => '收入',
|
||
'section' => '平台收入核对',
|
||
'title' => $combinedTitle,
|
||
'owner' => '晨曦',
|
||
'assignee' => '栎阳',
|
||
'start_date' => '2026-10-20',
|
||
'due_date' => '2026-10-21',
|
||
'status' => SystemizationTodo::STATUS_COMPLETED,
|
||
'completed_at' => '2026-10-21',
|
||
'progress' => '已完成',
|
||
'sort_order' => 2,
|
||
'source_key' => 'legacy-combined-row',
|
||
'imported_at' => now(),
|
||
'created_by' => user_id(),
|
||
'updated_by' => user_id(),
|
||
]);
|
||
Storage::disk('uploads')->put('systemization-todos/combined.png', 'image');
|
||
SystemizationTodoAttachment::query()->create([
|
||
'company_id' => (int) company_id(),
|
||
'systemization_todo_id' => $historical->id,
|
||
'path' => 'systemization-todos/combined.png',
|
||
'original_name' => 'combined.png',
|
||
'mime_type' => 'image/png',
|
||
'size' => 5,
|
||
'uploaded_by' => user_id(),
|
||
]);
|
||
|
||
$path = sys_get_temp_dir() . '/systemization-split-' . uniqid() . '.xlsx';
|
||
try {
|
||
$this->writeWorkbook($path, [
|
||
['收入', '平台收入核对', $combinedTitle, '晨曦', '栎阳', '2026-10-20', '2026-10-21', '已完成', null],
|
||
]);
|
||
$preview = app(SystemizationTodoWorkbookImporter::class)
|
||
->sync($path, (int) company_id(), user_id(), true);
|
||
$this->assertSame(2, $preview['created']);
|
||
$this->assertSame(1, $preview['updated']);
|
||
$this->assertSame(0, $preview['retained']);
|
||
|
||
$result = app(SystemizationTodoWorkbookImporter::class)
|
||
->sync($path, (int) company_id(), user_id());
|
||
$this->assertSame(2, $result['created']);
|
||
$this->assertSame(1, $result['updated']);
|
||
$this->assertSame(0, $result['retained']);
|
||
|
||
$todos = SystemizationTodo::query()->orderBy('sort_order')->get();
|
||
$this->assertSame([
|
||
'核对平台收入',
|
||
'核对订单收入',
|
||
'核对特殊订单',
|
||
], $todos->pluck('title')->all());
|
||
$this->assertSame($historical->id, $todos->first()->id);
|
||
$this->assertSame(1, $todos->first()->attachments()->count());
|
||
|
||
$secondResult = app(SystemizationTodoWorkbookImporter::class)
|
||
->sync($path, (int) company_id(), user_id());
|
||
$this->assertSame(0, $secondResult['created']);
|
||
$this->assertSame(0, $secondResult['updated']);
|
||
$this->assertSame(3, SystemizationTodo::query()->count());
|
||
} finally {
|
||
@unlink($path);
|
||
}
|
||
}
|
||
|
||
private function createTodo(
|
||
string $category,
|
||
string $section,
|
||
string $title,
|
||
string $dueDate,
|
||
int $sortOrder,
|
||
string $owner = '晨曦',
|
||
string $assignee = '栎阳',
|
||
): SystemizationTodo {
|
||
return SystemizationTodo::query()->create([
|
||
'company_id' => (int) company_id(),
|
||
'category' => $category,
|
||
'section' => $section,
|
||
'title' => $title,
|
||
'owner' => $owner,
|
||
'assignee' => $assignee,
|
||
'start_date' => '2026-10-25',
|
||
'due_date' => $dueDate,
|
||
'status' => SystemizationTodo::STATUS_PENDING,
|
||
'sort_order' => $sortOrder,
|
||
'created_by' => user_id(),
|
||
'updated_by' => user_id(),
|
||
]);
|
||
}
|
||
|
||
private function writeWorkbook(string $path, array $rows): void
|
||
{
|
||
$spreadsheet = new Spreadsheet();
|
||
$sheet = $spreadsheet->getActiveSheet();
|
||
$sheet->fromArray([
|
||
['类型', '内容', '待办事项(具体工作计划)', '总负责人', '责任人', '开始时间', '完成时间', '完成情况(放截图)', '备注'],
|
||
...$rows,
|
||
]);
|
||
(new Xlsx($spreadsheet))->save($path);
|
||
$spreadsheet->disconnectWorksheets();
|
||
}
|
||
|
||
private function pngUpload(string $name): UploadedFile
|
||
{
|
||
return UploadedFile::fake()->createWithContent(
|
||
$name,
|
||
base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='),
|
||
);
|
||
}
|
||
}
|