完善系统化待办筛选与展示 #664
@ -295,7 +295,7 @@ class SystemizationTodoWorkbookImporter
|
||||
[$finishDate, $finishNote] = $this->dateParts($finishText);
|
||||
$progress = $this->cell($cells, $columns['progress'] ?? null);
|
||||
$notes = $this->cell($cells, $columns['notes'] ?? null);
|
||||
$status = $this->status($progress, $startText, $finishText);
|
||||
$status = $this->status($progress);
|
||||
|
||||
foreach ($titles as $offset => $title) {
|
||||
$titleIndex = $offset + 1;
|
||||
@ -674,20 +674,12 @@ class SystemizationTodoWorkbookImporter
|
||||
return [null, mb_substr($value, 0, 100)];
|
||||
}
|
||||
|
||||
private function status(string $progress, string $startNote, string $finishNote): string
|
||||
private function status(string $progress): string
|
||||
{
|
||||
if (str_contains($progress, '已完成')) {
|
||||
return SystemizationTodo::STATUS_COMPLETED;
|
||||
}
|
||||
if ($progress !== '') {
|
||||
return str_contains($progress, '延期')
|
||||
? SystemizationTodo::STATUS_DELAYED
|
||||
: SystemizationTodo::STATUS_IN_PROGRESS;
|
||||
}
|
||||
if (str_contains($startNote, '延期') || str_contains($finishNote, '延期')) {
|
||||
return SystemizationTodo::STATUS_DELAYED;
|
||||
}
|
||||
|
||||
return SystemizationTodo::STATUS_PENDING;
|
||||
return SystemizationTodo::STATUS_INCOMPLETE;
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,10 +28,12 @@ class SystemizationTodos extends Controller
|
||||
->withCount('attachments');
|
||||
|
||||
if ($filters['status'] === self::OVERDUE_FILTER) {
|
||||
$query->whereNotIn('status', [SystemizationTodo::STATUS_COMPLETED])
|
||||
$query->where('status', '<>', SystemizationTodo::STATUS_COMPLETED)
|
||||
->whereNotNull('due_date')
|
||||
->whereDate('due_date', '<', today());
|
||||
} elseif ($filters['status'] !== '') {
|
||||
} elseif ($filters['status'] === SystemizationTodo::STATUS_INCOMPLETE) {
|
||||
$query->where('status', '<>', SystemizationTodo::STATUS_COMPLETED);
|
||||
} elseif ($filters['status'] === SystemizationTodo::STATUS_COMPLETED) {
|
||||
$query->where('status', $filters['status']);
|
||||
}
|
||||
if ($filters['category'] !== '') {
|
||||
@ -103,23 +105,28 @@ class SystemizationTodos extends Controller
|
||||
$todo->setAttribute('section_display', $sequence['section_display']);
|
||||
$todo->setAttribute('type_count', $sequence['type_count']);
|
||||
$todo->setAttribute('content_count', $sequence['content_count']);
|
||||
$todo->setAttribute('status_label', $todo->statusLabel());
|
||||
$reminder = $todo->reminder();
|
||||
$todo->setAttribute('reminder_label', $reminder['label']);
|
||||
$todo->setAttribute('reminder_tone', $reminder['tone']);
|
||||
});
|
||||
$items = $items->sortBy('hierarchy_order')->values();
|
||||
$positionGroups = $this->positionGroups($sequenceMap);
|
||||
|
||||
$base = SystemizationTodo::query();
|
||||
$totalCount = (clone $base)->count();
|
||||
$completedCount = (clone $base)
|
||||
->where('status', SystemizationTodo::STATUS_COMPLETED)
|
||||
->count();
|
||||
$incompleteCount = max(0, $totalCount - $completedCount);
|
||||
$categories = SystemizationTodo::query()
|
||||
->whereNotNull('category')
|
||||
->where('category', '<>', '')
|
||||
->distinct()
|
||||
->orderBy('category')
|
||||
->pluck('category');
|
||||
$statusCounts = (clone $base)
|
||||
->selectRaw('status, COUNT(*) AS aggregate')
|
||||
->groupBy('status')
|
||||
->pluck('aggregate', 'status');
|
||||
$overdueCount = (clone $base)
|
||||
->whereNotIn('status', [SystemizationTodo::STATUS_COMPLETED])
|
||||
->where('status', '<>', SystemizationTodo::STATUS_COMPLETED)
|
||||
->whereNotNull('due_date')
|
||||
->whereDate('due_date', '<', today())
|
||||
->count();
|
||||
@ -131,12 +138,10 @@ class SystemizationTodos extends Controller
|
||||
]);
|
||||
}
|
||||
foreach (SystemizationTodo::STATUSES as $status) {
|
||||
if ((int) $statusCounts->get($status, 0) > 0) {
|
||||
$statusOptions->push([
|
||||
'value' => $status,
|
||||
'label' => $status,
|
||||
]);
|
||||
}
|
||||
$statusOptions->push([
|
||||
'value' => $status,
|
||||
'label' => $status,
|
||||
]);
|
||||
}
|
||||
|
||||
return [
|
||||
@ -151,11 +156,14 @@ class SystemizationTodos extends Controller
|
||||
'todoAssignees' => $this->peopleOptions('assignee'),
|
||||
'todoStatuses' => SystemizationTodo::STATUSES,
|
||||
'todoStatusOptions' => $statusOptions,
|
||||
'todoReturnFilters' => base64_encode(json_encode(
|
||||
$filters,
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES,
|
||||
)),
|
||||
'todoSummary' => [
|
||||
'total' => (clone $base)->count(),
|
||||
'pending' => (int) $statusCounts->get(SystemizationTodo::STATUS_PENDING, 0),
|
||||
'in_progress' => (int) $statusCounts->get(SystemizationTodo::STATUS_IN_PROGRESS, 0),
|
||||
'completed' => (int) $statusCounts->get(SystemizationTodo::STATUS_COMPLETED, 0),
|
||||
'total' => $totalCount,
|
||||
'incomplete' => $incompleteCount,
|
||||
'completed' => $completedCount,
|
||||
'overdue' => $overdueCount,
|
||||
],
|
||||
'todoRoutes' => [
|
||||
@ -333,6 +341,7 @@ class SystemizationTodos extends Controller
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $this->validated($request);
|
||||
$this->normalizePendingDates($request, $data);
|
||||
$position = max(1, (int) ($data['position'] ?? 1));
|
||||
unset($data['position']);
|
||||
$data['company_id'] = (int) company_id();
|
||||
@ -506,6 +515,7 @@ class SystemizationTodos extends Controller
|
||||
public function update(Request $request, SystemizationTodo $todo): RedirectResponse
|
||||
{
|
||||
$data = $this->validated($request);
|
||||
$this->normalizePendingDates($request, $data);
|
||||
$targetPosition = max(0, (int) ($data['position'] ?? 0));
|
||||
unset($data['position']);
|
||||
$data['updated_by'] = user_id();
|
||||
@ -596,22 +606,30 @@ class SystemizationTodos extends Controller
|
||||
|
||||
private function filters(): array
|
||||
{
|
||||
$status = (string) request('status', '');
|
||||
|
||||
return [
|
||||
'status' => $status === self::OVERDUE_FILTER
|
||||
|| in_array($status, SystemizationTodo::STATUSES, true)
|
||||
? $status
|
||||
: '',
|
||||
'status' => $this->normalizeStatusValue(request('status')),
|
||||
'category' => trim((string) request('category', '')),
|
||||
'owners' => $this->peopleFilter('owner'),
|
||||
'assignees' => $this->peopleFilter('assignee'),
|
||||
'start_date' => $this->normalizeDateFilter(request('start_date')),
|
||||
'due_date' => $this->normalizeDateFilter(request('due_date')),
|
||||
'start_date' => $this->normalizeDateValue(request('start_date')),
|
||||
'due_date' => $this->normalizeDateValue(request('due_date')),
|
||||
'keyword' => trim((string) request('keyword', '')),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeStatusValue(mixed $value): string
|
||||
{
|
||||
$status = trim((string) $value);
|
||||
if (in_array($status, SystemizationTodo::LEGACY_INCOMPLETE_STATUSES, true)) {
|
||||
return SystemizationTodo::STATUS_INCOMPLETE;
|
||||
}
|
||||
|
||||
return $status === self::OVERDUE_FILTER
|
||||
|| in_array($status, SystemizationTodo::STATUSES, true)
|
||||
? $status
|
||||
: '';
|
||||
}
|
||||
|
||||
private function peopleFilter(string $key): array
|
||||
{
|
||||
return collect((array) request($key, []))
|
||||
@ -661,7 +679,7 @@ class SystemizationTodos extends Controller
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function normalizeDateFilter(mixed $value): string
|
||||
private function normalizeDateValue(mixed $value): string
|
||||
{
|
||||
$value = trim((string) $value);
|
||||
if ($value === '') {
|
||||
@ -686,8 +704,10 @@ class SystemizationTodos extends Controller
|
||||
'assignee' => ['nullable', 'string', 'max:255'],
|
||||
'start_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
'start_note' => ['nullable', 'string', 'max:50'],
|
||||
'start_pending' => ['nullable', 'boolean'],
|
||||
'due_date' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:start_date'],
|
||||
'due_note' => ['nullable', 'string', 'max:50'],
|
||||
'due_pending' => ['nullable', 'boolean'],
|
||||
'status' => ['required', Rule::in(SystemizationTodo::STATUSES)],
|
||||
'completed_at' => ['nullable', 'date_format:Y-m-d'],
|
||||
'progress' => ['nullable', 'string', 'max:10000'],
|
||||
@ -699,6 +719,19 @@ class SystemizationTodos extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
private function normalizePendingDates(Request $request, array &$data): void
|
||||
{
|
||||
foreach (['start', 'due'] as $prefix) {
|
||||
if ($request->boolean($prefix . '_pending')) {
|
||||
$data[$prefix . '_date'] = null;
|
||||
$data[$prefix . '_note'] = '待定';
|
||||
} elseif (!empty($data[$prefix . '_date'])) {
|
||||
$data[$prefix . '_note'] = null;
|
||||
}
|
||||
unset($data[$prefix . '_pending']);
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeCompletionDates(array &$data): void
|
||||
{
|
||||
$completed = ($data['status'] ?? '') === SystemizationTodo::STATUS_COMPLETED;
|
||||
@ -743,6 +776,7 @@ class SystemizationTodos extends Controller
|
||||
'section' => 'other',
|
||||
'page' => 'systemization-todos',
|
||||
];
|
||||
$parameters = array_merge($parameters, $this->returnedFilters());
|
||||
if ($focus) {
|
||||
$parameters['highlight'] = $focus->id;
|
||||
}
|
||||
@ -751,4 +785,51 @@ class SystemizationTodos extends Controller
|
||||
->route('business.pages.show', $parameters)
|
||||
->with($type === 'success' ? 'success' : 'error', $message);
|
||||
}
|
||||
|
||||
private function returnedFilters(): array
|
||||
{
|
||||
$encoded = request('return_filters');
|
||||
if (!is_string($encoded) || $encoded === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$json = base64_decode($encoded, true);
|
||||
$filters = $json === false ? null : json_decode($json, true);
|
||||
if (!is_array($filters)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$category = is_string($filters['category'] ?? null)
|
||||
? trim($filters['category'])
|
||||
: '';
|
||||
$keyword = is_string($filters['keyword'] ?? null)
|
||||
? trim($filters['keyword'])
|
||||
: '';
|
||||
$owners = collect((array) ($filters['owners'] ?? []))
|
||||
->filter(static fn (mixed $value): bool => is_string($value))
|
||||
->map(static fn (string $value): string => trim($value))
|
||||
->filter()
|
||||
->unique()
|
||||
->take(50)
|
||||
->values()
|
||||
->all();
|
||||
$assignees = collect((array) ($filters['assignees'] ?? []))
|
||||
->filter(static fn (mixed $value): bool => is_string($value))
|
||||
->map(static fn (string $value): string => trim($value))
|
||||
->filter()
|
||||
->unique()
|
||||
->take(50)
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return array_filter([
|
||||
'status' => $this->normalizeStatusValue($filters['status'] ?? null),
|
||||
'category' => $category,
|
||||
'owner' => $owners,
|
||||
'assignee' => $assignees,
|
||||
'start_date' => $this->normalizeDateValue($filters['start_date'] ?? null),
|
||||
'due_date' => $this->normalizeDateValue($filters['due_date'] ?? null),
|
||||
'keyword' => $keyword,
|
||||
], static fn (mixed $value): bool => $value !== '' && $value !== []);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,21 +7,24 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class SystemizationTodo extends Model
|
||||
{
|
||||
public const STATUS_PENDING = '待办';
|
||||
|
||||
public const STATUS_IN_PROGRESS = '进行中';
|
||||
public const STATUS_INCOMPLETE = '未完成';
|
||||
|
||||
public const STATUS_COMPLETED = '已完成';
|
||||
|
||||
public const STATUS_DELAYED = '已延期';
|
||||
|
||||
public const STATUSES = [
|
||||
self::STATUS_PENDING,
|
||||
self::STATUS_IN_PROGRESS,
|
||||
self::STATUS_INCOMPLETE,
|
||||
self::STATUS_COMPLETED,
|
||||
self::STATUS_DELAYED,
|
||||
];
|
||||
|
||||
public const LEGACY_INCOMPLETE_STATUSES = ['待办', '进行中', '已延期'];
|
||||
|
||||
// Keep old constant names for imports and existing callers without rewriting data.
|
||||
public const STATUS_PENDING = self::STATUS_INCOMPLETE;
|
||||
|
||||
public const STATUS_IN_PROGRESS = self::STATUS_INCOMPLETE;
|
||||
|
||||
public const STATUS_DELAYED = self::STATUS_INCOMPLETE;
|
||||
|
||||
protected $table = 'business_systemization_todos';
|
||||
|
||||
protected $fillable = [
|
||||
@ -65,4 +68,36 @@ class SystemizationTodo extends Model
|
||||
{
|
||||
return $this->status === self::STATUS_COMPLETED;
|
||||
}
|
||||
|
||||
public function statusLabel(): string
|
||||
{
|
||||
return $this->isCompleted() ? self::STATUS_COMPLETED : self::STATUS_INCOMPLETE;
|
||||
}
|
||||
|
||||
public function reminder(): array
|
||||
{
|
||||
if ($this->isCompleted()) {
|
||||
return ['label' => '已完成', 'tone' => 'muted'];
|
||||
}
|
||||
if ($this->due_date === null) {
|
||||
return ['label' => '时间待定', 'tone' => 'muted'];
|
||||
}
|
||||
|
||||
$today = today()->startOfDay();
|
||||
$dueDate = $this->due_date->copy()->startOfDay();
|
||||
if ($dueDate->lt($today)) {
|
||||
return [
|
||||
'label' => '逾期 ' . $dueDate->diffInDays($today) . ' 天',
|
||||
'tone' => 'danger',
|
||||
];
|
||||
}
|
||||
if ($dueDate->equalTo($today)) {
|
||||
return ['label' => '今天到期', 'tone' => 'warning'];
|
||||
}
|
||||
|
||||
return [
|
||||
'label' => '剩余 ' . $today->diffInDays($dueDate) . ' 天',
|
||||
'tone' => 'info',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
.sys-todo-page__button--primary:hover { background:#6a5129; color:#fff; }
|
||||
.sys-todo-page__button .material-icons-outlined { font-size:1rem; }
|
||||
.sys-todo-page__intro { margin:.35rem 0 0; color:var(--sys-muted); font-size:.75rem; line-height:1.6; }
|
||||
.sys-todo-summary { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); gap:.55rem; margin:.8rem 0; }
|
||||
.sys-todo-summary { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:.55rem; margin:.8rem 0; }
|
||||
.sys-todo-summary__item { display:flex; align-items:center; justify-content:space-between; gap:.6rem; padding:.5rem .7rem; border:1px solid #e6dfd2; border-radius:.5rem; background:#fffdf9; }
|
||||
.sys-todo-summary__label { color:var(--sys-muted); font-size:.72rem; }
|
||||
.sys-todo-summary__value { color:#171717; font-size:1rem; font-weight:700; font-variant-numeric:tabular-nums; }
|
||||
@ -38,42 +38,60 @@
|
||||
.sys-todo-filter__actions { display:flex; flex:0 0 auto; gap:.45rem; }
|
||||
.sys-todo-table-wrap { overflow-x:auto; overflow-y:visible; border:1px solid var(--sys-border); border-radius:.65rem; background:#fff; scrollbar-width:none; }
|
||||
.sys-todo-table-wrap::-webkit-scrollbar { height:0; }
|
||||
.sys-todo-table { width:100%; min-width:74rem; border-collapse:separate; border-spacing:0; font-size:.75rem; }
|
||||
.sys-todo-table th { position:sticky; top:0; z-index:4; padding:.55rem .48rem; border-bottom:1px solid var(--sys-border); background:#f3ece0; color:#3d3a35; text-align:left; white-space:nowrap; }
|
||||
.sys-todo-table th:first-child { left:0; z-index:8; width:5.4rem; min-width:5.4rem; text-align:center; box-shadow:1px 0 0 #e5ddcf; }
|
||||
.sys-todo-table td { padding:.72rem .6rem; border-bottom:1px solid #eee7da; background:#fff; color:#2f2a23; line-height:1.6; vertical-align:top; }
|
||||
.sys-todo-table { width:108rem; min-width:108rem; table-layout:fixed; border-collapse:separate; border-spacing:0; font-size:.75rem; }
|
||||
.sys-todo-table th { position:sticky; top:0; z-index:4; padding:.55rem .7rem; border-bottom:1px solid var(--sys-border); background:#f3ece0; color:#3d3a35; text-align:left; white-space:nowrap; }
|
||||
.sys-todo-table th:first-child { left:0; z-index:8; width:var(--sys-todo-type-width,5.4rem); text-align:center; box-shadow:1px 0 0 #e5ddcf; }
|
||||
.sys-todo-table td { height:3.5rem; padding:.72rem .7rem; border-bottom:1px solid #eee7da; background:#fff; color:#2f2a23; line-height:1.5; vertical-align:middle; }
|
||||
.sys-todo-table tbody tr { height:3.5rem; }
|
||||
.sys-todo-table tbody tr:hover td { background:#fdfbf6; }
|
||||
.sys-todo-table tbody tr.is-highlighted td { background:#fff4cf !important; }
|
||||
.sys-todo-table [data-sys-todo-row-edit], .sys-todo-table [data-sys-todo-group-edit] { cursor:pointer; }
|
||||
.sys-todo-table [data-sys-todo-row-edit]:hover, .sys-todo-table [data-sys-todo-group-edit]:hover { background:#fff4cf; }
|
||||
.sys-todo-table [data-sys-todo-row-edit]:focus-visible, .sys-todo-table [data-sys-todo-group-edit]:focus-visible { outline:2px solid var(--sys-gold); outline-offset:-2px; }
|
||||
.sys-todo-table__index-cell { width:4.8rem; min-width:4.8rem; text-align:center; }
|
||||
.sys-todo-table__index-cell { text-align:center; }
|
||||
.sys-todo-table__index-actions { display:inline-flex; align-items:center; justify-content:center; gap:.32rem; }
|
||||
.sys-todo-table__index { color:#6d5630; font-weight:600; font-variant-numeric:tabular-nums; white-space:nowrap; }
|
||||
.sys-todo-table__insert-after { display:inline-flex; align-items:center; justify-content:center; width:1.45rem; height:1.45rem; padding:0; border:1px solid #d8c9aa; border-radius:999px; background:#fffaf0; color:var(--sys-gold); cursor:pointer; }
|
||||
.sys-todo-table__insert-after:hover { border-color:var(--sys-gold); background:var(--sys-gold-soft); color:#6a5129; }
|
||||
.sys-todo-table__insert-after:focus-visible { outline:2px solid var(--sys-gold); outline-offset:2px; }
|
||||
.sys-todo-table__insert-after .material-icons-outlined { font-size:.95rem; line-height:1; }
|
||||
.sys-todo-table__group { padding-top:.9rem; padding-bottom:.9rem; background:#fcfaf5; font-weight:600; line-height:1.7; vertical-align:middle; }
|
||||
.sys-todo-table__group { padding-top:.9rem; padding-bottom:.9rem; background:#fcfaf5; font-weight:600; line-height:1.5; vertical-align:middle; }
|
||||
.sys-todo-table__group-content { display:inline-flex; align-items:flex-start; gap:.38rem; min-width:0; }
|
||||
.sys-todo-table__group-text { min-width:0; white-space:pre-line; overflow-wrap:anywhere; }
|
||||
.sys-todo-table__level { display:inline-flex; flex:0 0 auto; align-items:center; justify-content:center; min-width:1.35rem; height:1.35rem; margin-top:.05rem; border:1px solid #d8c9aa; border-radius:999px; background:#fffaf0; color:var(--sys-gold); font-size:.68rem; font-weight:700; font-variant-numeric:tabular-nums; }
|
||||
.sys-todo-table__type { position:sticky; left:0; z-index:3; width:5.4rem; min-width:5.4rem; text-align:center; white-space:nowrap; box-shadow:1px 0 0 #e5ddcf; }
|
||||
.sys-todo-table__type { position:sticky; left:0; z-index:3; width:var(--sys-todo-type-width,5.4rem); text-align:center; white-space:normal; box-shadow:1px 0 0 #e5ddcf; }
|
||||
.sys-todo-table__type .sys-todo-table__group-text { white-space:normal; }
|
||||
.sys-todo-table__section { min-width:11rem; max-width:17rem; }
|
||||
.sys-todo-table__title, .sys-todo-table__progress, .sys-todo-table__notes { line-height:1.45; white-space:pre-line; overflow-wrap:anywhere; }
|
||||
.sys-todo-table__title { min-width:17rem; max-width:24rem; font-weight:600; }
|
||||
.sys-todo-table__progress { min-width:12rem; max-width:20rem; }
|
||||
.sys-todo-table__notes { min-width:9rem; max-width:16rem; }
|
||||
.sys-todo-table__title, .sys-todo-table__progress-notes { line-height:1.5; white-space:pre-line; overflow-wrap:anywhere; }
|
||||
.sys-todo-table__title { font-weight:600; }
|
||||
.sys-todo-table__inline-add { display:inline-flex; align-items:center; justify-content:center; gap:.15rem; min-height:1.65rem; margin-top:.45rem; padding:0 .45rem; border:1px solid #d8c9aa; border-radius:999px; background:#fffaf0; color:#6d5630; font:inherit; font-size:.68rem; font-weight:500; white-space:nowrap; cursor:pointer; }
|
||||
.sys-todo-table__inline-add:hover { border-color:var(--sys-gold); background:var(--sys-gold-soft); color:#6a5129; }
|
||||
.sys-todo-table__inline-add:focus-visible { outline:2px solid var(--sys-gold); outline-offset:2px; }
|
||||
.sys-todo-table__inline-actions { display:flex; flex-wrap:wrap; justify-content:center; gap:.25rem; }
|
||||
.sys-todo-table__inline-actions .sys-todo-table__inline-add { margin-top:.3rem; }
|
||||
.sys-todo-table__progress-notes { display:grid; gap:.35rem; }
|
||||
.sys-todo-table__detail { display:grid; gap:.15rem; }
|
||||
.sys-todo-table__detail-label { color:#856632; font-size:.66rem; font-weight:600; }
|
||||
.sys-todo-table__reminder { display:inline-flex; align-items:center; padding:.1rem .45rem; border:1px solid #dfd6c5; border-radius:999px; background:#faf6ee; color:#51483c; white-space:nowrap; }
|
||||
.sys-todo-table__reminder.is-danger { border-color:#e7c3c3; background:#fff3f3; color:#b80000; }
|
||||
.sys-todo-table__reminder.is-warning { border-color:#e5c98f; background:#fff8e8; color:#935f07; }
|
||||
.sys-todo-table__reminder.is-info { border-color:#b5cad8; background:#f3f8fb; color:#006395; }
|
||||
.sys-todo-table__reminder.is-muted { color:#68625a; }
|
||||
.sys-todo-table__col-resizer { position:absolute; top:0; right:-4px; z-index:12; width:9px; height:100%; padding:0; border:0; background:transparent; cursor:col-resize; touch-action:none; }
|
||||
.sys-todo-table__col-resizer::after { position:absolute; top:20%; right:4px; width:1px; height:60%; background:#bfb3a0; content:""; }
|
||||
.sys-todo-table__col-resizer:hover::after, .sys-todo-table__col-resizer:focus-visible::after { top:0; width:2px; height:100%; background:var(--sys-gold); }
|
||||
.sys-todo-table__col-resizer:focus-visible { outline:none; }
|
||||
.sys-todo-badge { display:inline-flex; align-items:center; padding:.1rem .45rem; border:1px solid #dfd6c5; border-radius:999px; background:#faf6ee; color:#51483c; white-space:nowrap; }
|
||||
.sys-todo-badge--progress { border-color:#dbbf7a; background:#fff7df; color:#935f07; }
|
||||
.sys-todo-badge--incomplete { border-color:#dbbf7a; background:#fff7df; color:#935f07; }
|
||||
.sys-todo-badge--completed { border-color:#bad0ac; background:#f2f8ed; color:#53793e; }
|
||||
.sys-todo-badge--delayed { border-color:#e7c3c3; background:#fff3f3; color:#b80000; }
|
||||
.sys-todo-badge--overdue { border-color:#e7c3c3; color:#b80000; }
|
||||
.sys-todo-shots { display:flex; flex-wrap:wrap; gap:.3rem; max-width:13rem; }
|
||||
.sys-todo-shots button { display:block; width:4.25rem; height:3.2rem; padding:0; border:1px solid #ddd4c4; border-radius:.35rem; overflow:hidden; background:#faf8f3; cursor:zoom-in; }
|
||||
.sys-todo-shots img { width:100%; height:100%; object-fit:cover; }
|
||||
.sys-todo-table__add { margin-top:1rem; padding:.55rem .7rem; border:1px dashed #d8cdb8; border-radius:.55rem; background:#fcfaf5; color:#666; font-size:.72rem; }
|
||||
.sys-todo-table__add { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.5rem .75rem; margin-top:1rem; padding:.55rem .7rem; border:1px dashed #d8cdb8; border-radius:.55rem; background:#fcfaf5; color:#666; font-size:.72rem; }
|
||||
.sys-todo-table__add-copy { min-width:16rem; flex:1 1 30rem; }
|
||||
.sys-todo-table__reset-widths { min-height:1.8rem; padding:0 .55rem; border:1px solid var(--sys-border); border-radius:.4rem; background:#fff; color:#51483c; font:inherit; font-size:.7rem; cursor:pointer; }
|
||||
.sys-todo-table__reset-widths:hover { border-color:var(--sys-gold); background:var(--sys-gold-soft); }
|
||||
.sys-todo-table__footer-add { background:#fcfaf5; text-align:center; }
|
||||
.sys-todo-table__footer-note { padding-left:1rem; color:#666; font-size:.7rem; font-weight:400; text-align:left; }
|
||||
.sys-todo-scrollrail { position:fixed; bottom:.85rem; z-index:35; height:1.15rem; padding:2px; border:1px solid #d8cdb8; border-radius:999px; background:rgba(243,236,224,.96); box-shadow:0 5px 18px rgba(23,23,23,.16); opacity:0; pointer-events:none; overflow-x:scroll; overflow-y:hidden; transition:opacity 150ms ease; }
|
||||
.sys-todo-scrollrail.is-visible { opacity:1; pointer-events:auto; }
|
||||
.sys-todo-scrollrail__spacer { height:1px; }
|
||||
@ -96,6 +114,8 @@
|
||||
.sys-todo-dialog__body { max-height:calc(90vh - 9rem); overflow:auto; padding:1rem 1.25rem; }
|
||||
.sys-todo-form-grid { display:grid; grid-template-columns:repeat(12,minmax(0,1fr)); gap:.75rem 1rem; }
|
||||
.sys-todo-field { display:grid; gap:.3rem; min-width:0; color:#595959; font-size:.75rem; }
|
||||
.sys-todo-combined-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.65rem; padding:.65rem; border:1px solid var(--sys-border); border-radius:.55rem; background:#fcfaf5; }
|
||||
.sys-todo-combined-grid label { display:grid; gap:.3rem; min-width:0; }
|
||||
.sys-todo-field--2 { grid-column:span 2; }
|
||||
.sys-todo-field--3 { grid-column:span 3; }
|
||||
.sys-todo-field--4 { grid-column:span 4; }
|
||||
@ -165,9 +185,9 @@
|
||||
.sys-todo-image-dialog__body img { max-width:100%; max-height:100%; object-fit:contain; }
|
||||
.sys-todo-image-dialog__foot { display:flex; justify-content:flex-end; padding:.55rem .85rem; background:#1d1d1f; }
|
||||
.sys-todo-image-dialog__foot a { color:#f0e4bf; font-size:.75rem; }
|
||||
@media (max-width:1300px) { .sys-todo-summary { grid-template-columns:repeat(3,minmax(0,1fr)); } }
|
||||
@media (max-width:1300px) { .sys-todo-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } }
|
||||
@media (max-width:900px) { .sys-todo-filter label, .sys-todo-filter label.sys-todo-filter__keyword { flex-basis:9rem; } .sys-todo-filter__actions { width:100%; } .sys-todo-filter__actions .sys-todo-page__button { flex:1; } }
|
||||
@media (max-width:720px) { .sys-todo-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } .sys-todo-filter label, .sys-todo-filter label.sys-todo-filter__keyword { flex-basis:10rem; } .sys-todo-field--2,.sys-todo-field--3,.sys-todo-field--4,.sys-todo-field--6 { grid-column:span 12; } }
|
||||
@media (max-width:720px) { .sys-todo-summary { grid-template-columns:repeat(2,minmax(0,1fr)); } .sys-todo-filter label, .sys-todo-filter label.sys-todo-filter__keyword { flex-basis:10rem; } .sys-todo-field--2,.sys-todo-field--3,.sys-todo-field--4,.sys-todo-field--6 { grid-column:span 12; } .sys-todo-combined-grid { grid-column:span 12; grid-template-columns:1fr; } }
|
||||
</style>
|
||||
|
||||
<div class="biz-card-head">
|
||||
@ -177,12 +197,7 @@
|
||||
<span class="material-icons-outlined" aria-hidden="true">checklist</span>
|
||||
<span>财务系统化待办</span>
|
||||
</div>
|
||||
<p class="sys-todo-page__intro">日常过会使用。新增事项、跟进责任人和完成情况,截图直接附在该事项下。</p>
|
||||
</div>
|
||||
<div class="sys-todo-page__head-actions">
|
||||
<button type="button" class="sys-todo-page__button sys-todo-page__button--primary" data-sys-todo-create>
|
||||
<span class="material-icons-outlined" aria-hidden="true">add</span>新增待办
|
||||
</button>
|
||||
<p class="sys-todo-page__intro">日常过会使用。在类型、内容或序号旁直接添加,筛选修改后仍保持当前筛选结果。</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -196,8 +211,7 @@
|
||||
|
||||
<div class="sys-todo-summary" aria-label="系统化待办统计">
|
||||
<div class="sys-todo-summary__item"><div class="sys-todo-summary__label">全部</div><div class="sys-todo-summary__value">{{ number_format($todoSummary['total']) }}</div></div>
|
||||
<div class="sys-todo-summary__item"><div class="sys-todo-summary__label">待办</div><div class="sys-todo-summary__value">{{ number_format($todoSummary['pending']) }}</div></div>
|
||||
<div class="sys-todo-summary__item"><div class="sys-todo-summary__label">进行中</div><div class="sys-todo-summary__value">{{ number_format($todoSummary['in_progress']) }}</div></div>
|
||||
<div class="sys-todo-summary__item is-warning"><div class="sys-todo-summary__label">未完成</div><div class="sys-todo-summary__value">{{ number_format($todoSummary['incomplete']) }}</div></div>
|
||||
<div class="sys-todo-summary__item is-success"><div class="sys-todo-summary__label">已完成</div><div class="sys-todo-summary__value">{{ number_format($todoSummary['completed']) }}</div></div>
|
||||
<div class="sys-todo-summary__item is-warning"><div class="sys-todo-summary__label">已逾期</div><div class="sys-todo-summary__value">{{ number_format($todoSummary['overdue']) }}</div></div>
|
||||
</div>
|
||||
@ -206,6 +220,7 @@
|
||||
<summary>从 Excel 同步待办(新增并更新工作内容,保留完成截图)</summary>
|
||||
<form method="POST" action="{{ $todoRoutes['import'] }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
<input type="hidden" name="return_filters" value="{{ $todoReturnFilters }}">
|
||||
<label>待办 Excel<input type="file" name="source" accept=".xlsx" required></label>
|
||||
<button type="submit" class="sys-todo-page__button">同步</button>
|
||||
</form>
|
||||
@ -337,29 +352,48 @@
|
||||
@endphp
|
||||
|
||||
<div class="sys-todo-table__add">
|
||||
共 {{ number_format($items->count()) }} 条,已全部显示。点击序号旁的+可在该条之后插入,原后续序号自动顺延;点击待办事项编辑详情,点击类型或内容可单独修改该组名称。
|
||||
<span class="sys-todo-table__add-copy">共 {{ number_format($items->count()) }} 条,已全部显示。在类型、内容下方的+直接新增;拖动表头分隔线可调整列宽,双击分隔线可恢复该列。</span>
|
||||
<button type="button" class="sys-todo-table__reset-widths" data-sys-todo-reset-columns>恢复全部默认列宽</button>
|
||||
</div>
|
||||
|
||||
<div class="sys-todo-table-wrap" tabindex="0" aria-label="待办列表,可横向滚动查看全部内容" data-sys-todo-table-wrap>
|
||||
<table class="sys-todo-table" aria-label="财务系统化待办清单">
|
||||
<table class="sys-todo-table" aria-label="财务系统化待办清单" style="--sys-todo-type-width:94px">
|
||||
<colgroup>
|
||||
<col data-sys-todo-col="type" data-default-width="94" data-min-width="72" style="width:94px">
|
||||
<col data-sys-todo-col="section" data-default-width="212" data-min-width="120" style="width:212px">
|
||||
<col data-sys-todo-col="sequence" data-default-width="94" data-min-width="80" style="width:94px">
|
||||
<col data-sys-todo-col="title" data-default-width="330" data-min-width="180" style="width:330px">
|
||||
<col data-sys-todo-col="owner" data-default-width="110" data-min-width="80" style="width:110px">
|
||||
<col data-sys-todo-col="assignee" data-default-width="110" data-min-width="80" style="width:110px">
|
||||
<col data-sys-todo-col="start_date" data-default-width="112" data-min-width="88" style="width:112px">
|
||||
<col data-sys-todo-col="due_date" data-default-width="112" data-min-width="88" style="width:112px">
|
||||
<col data-sys-todo-col="reminder" data-default-width="116" data-min-width="96" style="width:116px">
|
||||
<col data-sys-todo-col="status" data-default-width="92" data-min-width="80" style="width:92px">
|
||||
<col data-sys-todo-col="progress_notes" data-default-width="260" data-min-width="180" style="width:260px">
|
||||
<col data-sys-todo-col="screenshots" data-default-width="110" data-min-width="90" style="width:110px">
|
||||
</colgroup>
|
||||
<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" data-sys-todo-column="type">类型<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="type" aria-label="调整类型列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="section">内容<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="section" aria-label="调整内容列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="sequence">待办事项序号<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="sequence" aria-label="调整序号列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="title">待办事项<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="title" aria-label="调整待办事项列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="owner">总负责人<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="owner" aria-label="调整总负责人列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="assignee">责任人<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="assignee" aria-label="调整责任人列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="start_date">开始时间<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="start_date" aria-label="调整开始时间列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="due_date">完成时间<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="due_date" aria-label="调整完成时间列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="reminder">提醒<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="reminder" aria-label="调整提醒列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="status">状态<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="status" aria-label="调整状态列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="progress_notes">完成情况 / 备注<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="progress_notes" aria-label="调整完成情况和备注列宽" title="拖动调整,双击恢复"></button></th>
|
||||
<th scope="col" data-sys-todo-column="screenshots">完成截图<button type="button" class="sys-todo-table__col-resizer" data-sys-todo-col-resizer="screenshots" aria-label="调整完成截图列宽" title="拖动调整,双击恢复"></button></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($items as $item)
|
||||
@php
|
||||
$isOverdue = ! $item->isCompleted()
|
||||
&& $item->due_date !== null
|
||||
&& $item->due_date->isBefore(today());
|
||||
$statusClass = match ($item->status) {
|
||||
\App\Models\Business\SystemizationTodo::STATUS_IN_PROGRESS => 'sys-todo-badge--progress',
|
||||
\App\Models\Business\SystemizationTodo::STATUS_COMPLETED => 'sys-todo-badge--completed',
|
||||
\App\Models\Business\SystemizationTodo::STATUS_DELAYED => 'sys-todo-badge--delayed',
|
||||
default => '',
|
||||
};
|
||||
$statusClass = $item->isCompleted()
|
||||
? 'sys-todo-badge--completed'
|
||||
: 'sys-todo-badge--incomplete';
|
||||
$todoPayload = [
|
||||
'id' => $item->id,
|
||||
'position' => (int) $item->item_sequence,
|
||||
@ -372,7 +406,7 @@
|
||||
'start_note' => $item->start_note ?? '',
|
||||
'due_date' => $item->due_date?->format('Y-m-d') ?? '',
|
||||
'due_note' => $item->due_note ?? '',
|
||||
'status' => $item->status,
|
||||
'status' => $item->status_label,
|
||||
'completed_at' => $item->completed_at?->format('Y-m-d') ?? '',
|
||||
'progress' => $item->progress ?? '',
|
||||
'notes' => $item->notes ?? '',
|
||||
@ -403,6 +437,15 @@
|
||||
<span class="sys-todo-table__level" title="类型序号">{{ (int) $item->type_sequence }}</span>
|
||||
<span class="sys-todo-table__group-text">{{ $item->category_display ?: '未分类' }}</span>
|
||||
</span>
|
||||
<span class="sys-todo-table__inline-actions">
|
||||
<button type="button" class="sys-todo-table__inline-add"
|
||||
data-sys-todo-inline-add
|
||||
data-sys-todo-add-mode="type">+ 类型</button>
|
||||
<button type="button" class="sys-todo-table__inline-add"
|
||||
data-sys-todo-inline-add
|
||||
data-sys-todo-add-mode="content"
|
||||
data-sys-todo-add-category="{{ $item->category ?? '' }}">+ 内容</button>
|
||||
</span>
|
||||
</td>
|
||||
@endif
|
||||
@if ($sectionSpan !== 0)
|
||||
@ -418,6 +461,13 @@
|
||||
<span class="sys-todo-table__level" title="内容序号">{{ (int) $item->content_sequence }}</span>
|
||||
<span class="sys-todo-table__group-text">{{ $sectionLabels[$loop->index] ?: '未填写内容' }}</span>
|
||||
</span>
|
||||
<button type="button" class="sys-todo-table__inline-add"
|
||||
data-sys-todo-inline-add
|
||||
data-sys-todo-add-mode="todo"
|
||||
data-sys-todo-add-category="{{ $item->category ?? '' }}"
|
||||
data-sys-todo-add-section="{{ $sectionLabels[$loop->index] ?? '' }}"
|
||||
data-sys-todo-add-position="{{ (int) $item->content_count + 1 }}"
|
||||
data-sys-todo-add-after-sequence="{{ (int) $item->content_count }}">+ 待办</button>
|
||||
</td>
|
||||
@endif
|
||||
<td class="sys-todo-table__index-cell">
|
||||
@ -442,12 +492,27 @@
|
||||
<td>{{ $item->owner ?: '—' }}</td>
|
||||
<td>{{ $item->assignee ?: '—' }}</td>
|
||||
<td>{{ $item->start_date?->format('Y-m-d') ?: ($item->start_note ?: '—') }}</td>
|
||||
<td>
|
||||
{{ $item->due_date?->format('Y-m-d') ?: ($item->due_note ?: '—') }}
|
||||
@if ($isOverdue)<br><span class="sys-todo-badge sys-todo-badge--overdue">已逾期</span>@endif
|
||||
<td>{{ $item->due_date?->format('Y-m-d') ?: ($item->due_note ?: '—') }}</td>
|
||||
<td><span class="sys-todo-table__reminder is-{{ $item->reminder_tone }}">{{ $item->reminder_label }}</span></td>
|
||||
<td><span class="sys-todo-badge {{ $statusClass }}">{{ $item->status_label }}</span></td>
|
||||
<td class="sys-todo-table__progress-notes">
|
||||
@if ($item->progress || $item->notes)
|
||||
@if ($item->progress)
|
||||
<div class="sys-todo-table__detail">
|
||||
<span class="sys-todo-table__detail-label">完成情况</span>
|
||||
<span>{{ $item->progress }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@if ($item->notes)
|
||||
<div class="sys-todo-table__detail">
|
||||
<span class="sys-todo-table__detail-label">备注</span>
|
||||
<span>{{ $item->notes }}</span>
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td><span class="sys-todo-badge {{ $statusClass }}">{{ $item->status }}</span></td>
|
||||
<td class="sys-todo-table__progress">{{ $item->progress ?: '—' }}</td>
|
||||
<td>
|
||||
@if ($item->attachments->isNotEmpty())
|
||||
<div class="sys-todo-shots">
|
||||
@ -468,12 +533,21 @@
|
||||
—
|
||||
@endif
|
||||
</td>
|
||||
<td class="sys-todo-table__notes">{{ $item->notes ?: '—' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="12"><div class="sys-todo-empty">当前筛选条件下没有待办事项。</div></td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="sys-todo-table__footer-add sys-todo-table__type">
|
||||
<button type="button" class="sys-todo-table__inline-add"
|
||||
data-sys-todo-inline-add
|
||||
data-sys-todo-add-mode="type">+ 类型</button>
|
||||
</td>
|
||||
<td colspan="11" class="sys-todo-table__footer-add sys-todo-table__footer-note">也可在现有类型下方新增内容,在内容下方新增待办。</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="sys-todo-scrollrail" tabindex="0" aria-label="表格横向滚动条" data-sys-todo-scrollrail>
|
||||
@ -484,6 +558,7 @@
|
||||
<form method="POST" enctype="multipart/form-data" data-sys-todo-form>
|
||||
@csrf
|
||||
<input type="hidden" name="_method" value="POST" data-sys-todo-method>
|
||||
<input type="hidden" name="return_filters" value="{{ $todoReturnFilters }}">
|
||||
<div class="sys-todo-dialog__head">
|
||||
<h2 id="sysTodoDialogTitle">新增待办</h2>
|
||||
<button type="button" class="sys-todo-dialog__close" data-sys-todo-close aria-label="关闭">×</button>
|
||||
@ -539,7 +614,7 @@
|
||||
<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">
|
||||
<input type="checkbox" name="start_pending" value="1" data-sys-todo-date-pending="start">
|
||||
<span>待定</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -551,7 +626,7 @@
|
||||
<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">
|
||||
<input type="checkbox" name="due_pending" value="1" data-sys-todo-date-pending="due">
|
||||
<span>待定</span>
|
||||
</label>
|
||||
</div>
|
||||
@ -559,8 +634,12 @@
|
||||
<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>
|
||||
<div class="sys-todo-field sys-todo-field--12">完成情况与备注
|
||||
<div class="sys-todo-combined-grid">
|
||||
<label>完成情况<textarea name="progress" maxlength="10000" data-sys-todo-auto-grow></textarea></label>
|
||||
<label>备注<textarea name="notes" maxlength="10000" data-sys-todo-auto-grow></textarea></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sys-todo-field sys-todo-field--6" data-sys-todo-upload>
|
||||
<span>完成截图</span>
|
||||
<div class="sys-todo-upload-zone">
|
||||
@ -584,6 +663,7 @@
|
||||
<form method="POST" data-sys-todo-group-form>
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
<input type="hidden" name="return_filters" value="{{ $todoReturnFilters }}">
|
||||
<div class="sys-todo-group-dialog__head">
|
||||
<h2 id="sysTodoGroupDialogTitle" data-sys-todo-group-title>修改类型</h2>
|
||||
<button type="button" data-sys-todo-group-close aria-label="关闭">×</button>
|
||||
@ -606,6 +686,7 @@
|
||||
<form method="POST" data-sys-todo-delete-form>
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<input type="hidden" name="return_filters" value="{{ $todoReturnFilters }}">
|
||||
<div class="sys-todo-confirm__head">
|
||||
<span class="material-icons-outlined" aria-hidden="true">warning</span>
|
||||
<span id="sysTodoDeleteConfirmTitle">确认删除待办</span>
|
||||
@ -700,9 +781,20 @@
|
||||
var assigneeFilter = document.querySelector('[data-sys-todo-assignee-filter]');
|
||||
var assigneeSummary = assigneeFilter.querySelector('[data-sys-todo-assignee-summary]');
|
||||
var tableWrap = document.querySelector('[data-sys-todo-table-wrap]');
|
||||
var todoTable = tableWrap.querySelector('.sys-todo-table');
|
||||
var todoColumns = Array.prototype.slice.call(todoTable.querySelectorAll('[data-sys-todo-col]'));
|
||||
var todoColumnResizers = Array.prototype.slice.call(
|
||||
todoTable.querySelectorAll('[data-sys-todo-col-resizer]')
|
||||
);
|
||||
var resetColumnsButton = document.querySelector('[data-sys-todo-reset-columns]');
|
||||
var scrollRail = document.querySelector('[data-sys-todo-scrollrail]');
|
||||
var scrollRailSpacer = document.querySelector('[data-sys-todo-scrollrail-spacer]');
|
||||
var scrollRailFrame = null;
|
||||
var columnStorageKey = 'caiwu-systemization-todo-column-widths-v1';
|
||||
var defaultColumnWidths = todoColumns.map(function (column) {
|
||||
return Number(column.getAttribute('data-default-width') || 100);
|
||||
});
|
||||
var columnWidths = defaultColumnWidths.slice();
|
||||
|
||||
function decodePayload(value) {
|
||||
var binary = atob(value);
|
||||
@ -836,6 +928,96 @@
|
||||
});
|
||||
}
|
||||
|
||||
function applyColumnWidths(widths, persist) {
|
||||
var total = 0;
|
||||
columnWidths = todoColumns.map(function (column, index) {
|
||||
var defaultWidth = defaultColumnWidths[index];
|
||||
var minimum = Number(column.getAttribute('data-min-width') || 64);
|
||||
var width = Math.min(720, Math.max(minimum, Number(widths[index] || defaultWidth)));
|
||||
column.style.width = width + 'px';
|
||||
total += width;
|
||||
return width;
|
||||
});
|
||||
todoTable.style.width = total + 'px';
|
||||
todoTable.style.minWidth = total + 'px';
|
||||
todoTable.style.setProperty('--sys-todo-type-width', columnWidths[0] + 'px');
|
||||
if (persist) {
|
||||
try {
|
||||
window.localStorage.setItem(columnStorageKey, JSON.stringify(columnWidths));
|
||||
} catch (error) {
|
||||
// Column resizing still works when browser storage is unavailable.
|
||||
}
|
||||
}
|
||||
scheduleScrollRailSync();
|
||||
}
|
||||
|
||||
function resetColumnWidth(index) {
|
||||
var widths = columnWidths.slice();
|
||||
widths[index] = defaultColumnWidths[index];
|
||||
applyColumnWidths(widths, true);
|
||||
}
|
||||
|
||||
try {
|
||||
var storedColumnWidths = JSON.parse(window.localStorage.getItem(columnStorageKey) || 'null');
|
||||
if (Array.isArray(storedColumnWidths) && storedColumnWidths.length === todoColumns.length) {
|
||||
applyColumnWidths(storedColumnWidths, false);
|
||||
} else {
|
||||
applyColumnWidths(defaultColumnWidths, false);
|
||||
}
|
||||
} catch (error) {
|
||||
applyColumnWidths(defaultColumnWidths, false);
|
||||
}
|
||||
|
||||
todoColumnResizers.forEach(function (handle) {
|
||||
var columnName = handle.getAttribute('data-sys-todo-col-resizer');
|
||||
var columnIndex = todoColumns.findIndex(function (column) {
|
||||
return column.getAttribute('data-sys-todo-col') === columnName;
|
||||
});
|
||||
if (columnIndex === -1) return;
|
||||
|
||||
var drag = null;
|
||||
handle.addEventListener('pointerdown', function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
drag = {
|
||||
pointerId: event.pointerId,
|
||||
startX: event.clientX,
|
||||
startWidth: columnWidths[columnIndex],
|
||||
};
|
||||
handle.setPointerCapture(event.pointerId);
|
||||
document.body.style.userSelect = 'none';
|
||||
});
|
||||
handle.addEventListener('pointermove', function (event) {
|
||||
if (!drag || drag.pointerId !== event.pointerId) return;
|
||||
var widths = columnWidths.slice();
|
||||
widths[columnIndex] = drag.startWidth + event.clientX - drag.startX;
|
||||
applyColumnWidths(widths, false);
|
||||
});
|
||||
function finishDrag(event) {
|
||||
if (!drag || drag.pointerId !== event.pointerId) return;
|
||||
drag = null;
|
||||
document.body.style.userSelect = '';
|
||||
applyColumnWidths(columnWidths, true);
|
||||
}
|
||||
handle.addEventListener('pointerup', finishDrag);
|
||||
handle.addEventListener('pointercancel', finishDrag);
|
||||
handle.addEventListener('dblclick', function (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
resetColumnWidth(columnIndex);
|
||||
});
|
||||
handle.addEventListener('keydown', function (event) {
|
||||
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return;
|
||||
event.preventDefault();
|
||||
var widths = columnWidths.slice();
|
||||
widths[columnIndex] += event.key === 'ArrowLeft' ? -16 : 16;
|
||||
applyColumnWidths(widths, true);
|
||||
});
|
||||
});
|
||||
resetColumnsButton.addEventListener('click', function () {
|
||||
applyColumnWidths(defaultColumnWidths, true);
|
||||
});
|
||||
|
||||
function selectedFilesLabel() {
|
||||
return Array.prototype.slice.call(screenshotInput.files || [])
|
||||
.map(function (file) { return file.name; })
|
||||
@ -1074,7 +1256,7 @@
|
||||
applyDateControl(dueDateControl, '', '');
|
||||
}
|
||||
if (!currentTodoId && !(payload && payload.status)) {
|
||||
setValue('status', '待办');
|
||||
setValue('status', '未完成');
|
||||
}
|
||||
populatePositionOptions();
|
||||
if (currentTodoId) {
|
||||
@ -1154,11 +1336,23 @@
|
||||
);
|
||||
return;
|
||||
}
|
||||
var create = event.target.closest('[data-sys-todo-create]');
|
||||
if (create) {
|
||||
var inlineAdd = event.target.closest('[data-sys-todo-inline-add]');
|
||||
if (inlineAdd) {
|
||||
event.preventDefault();
|
||||
var addMode = inlineAdd.getAttribute('data-sys-todo-add-mode') || 'todo';
|
||||
openDialog({
|
||||
category: create.getAttribute('data-sys-todo-default-category') || '',
|
||||
section: create.getAttribute('data-sys-todo-default-section') || '',
|
||||
category: addMode === 'type'
|
||||
? ''
|
||||
: (inlineAdd.getAttribute('data-sys-todo-add-category') || ''),
|
||||
section: addMode === 'todo'
|
||||
? (inlineAdd.getAttribute('data-sys-todo-add-section') || '')
|
||||
: '',
|
||||
insertPosition: addMode === 'todo'
|
||||
? Number(inlineAdd.getAttribute('data-sys-todo-add-position') || 1)
|
||||
: 1,
|
||||
afterSequence: addMode === 'todo'
|
||||
? Number(inlineAdd.getAttribute('data-sys-todo-add-after-sequence') || 0)
|
||||
: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ 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;
|
||||
@ -33,9 +34,15 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
->assertSeeText('开始时间')
|
||||
->assertSeeText('完成时间')
|
||||
->assertSeeText('待定')
|
||||
->assertSee('data-sys-todo-create', false)
|
||||
->assertSeeText('完成情况 / 备注')
|
||||
->assertSeeText('提醒')
|
||||
->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('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)
|
||||
@ -55,7 +62,7 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
->assertDontSee('sys-todo-table__clamp', false)
|
||||
->assertSeeText('+ 新增类型')
|
||||
->assertSeeText('+ 新增内容')
|
||||
->assertSeeText('点击序号旁的+可在该条之后插入')
|
||||
->assertSeeText('在类型、内容下方的+直接新增')
|
||||
->assertSee('data-sys-todo-choose-files', false)
|
||||
->assertSee('data-sys-todo-open-delete', false)
|
||||
->assertSee('id="sysTodoDeleteConfirm"', false)
|
||||
@ -65,9 +72,11 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
->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(
|
||||
1,
|
||||
substr_count((string) $pageResponse->getContent(), 'data-sys-todo-create>'),
|
||||
[SystemizationTodo::STATUS_INCOMPLETE, SystemizationTodo::STATUS_COMPLETED],
|
||||
$pageData['todoStatuses'],
|
||||
);
|
||||
|
||||
$storeResponse = $this->post(route('business.systemization-todos.store'), [
|
||||
@ -78,7 +87,7 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
'assignee' => '栎阳',
|
||||
'start_date' => '2026-10-25',
|
||||
'due_date' => '2026-10-28',
|
||||
'status' => SystemizationTodo::STATUS_IN_PROGRESS,
|
||||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||||
'progress' => '已拉取账单,待核对订单',
|
||||
'notes' => '周会跟进',
|
||||
'screenshots' => [
|
||||
@ -94,7 +103,7 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
$this->assertSame((string) $todo->id, $redirectQuery['highlight'] ?? null);
|
||||
$this->assertSame('收入', $todo->category);
|
||||
$this->assertSame('晨曦、人参', $todo->owner);
|
||||
$this->assertSame(SystemizationTodo::STATUS_IN_PROGRESS, $todo->status);
|
||||
$this->assertSame(SystemizationTodo::STATUS_INCOMPLETE, $todo->status);
|
||||
$this->assertSame(2, $todo->attachments()->count());
|
||||
$this->get(route('business.pages.show', [
|
||||
'section' => 'other',
|
||||
@ -372,10 +381,10 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
'owner' => '晨曦',
|
||||
'assignee' => '栎阳',
|
||||
'start_date' => null,
|
||||
'start_note' => '待定',
|
||||
'start_pending' => 1,
|
||||
'due_date' => null,
|
||||
'due_note' => '待定',
|
||||
'status' => SystemizationTodo::STATUS_PENDING,
|
||||
'due_pending' => 1,
|
||||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||||
'progress' => '',
|
||||
'notes' => '',
|
||||
])->assertRedirect();
|
||||
@ -393,10 +402,10 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
'owner' => '晨曦',
|
||||
'assignee' => '栎阳',
|
||||
'start_date' => null,
|
||||
'start_note' => '待定',
|
||||
'start_pending' => 1,
|
||||
'due_date' => null,
|
||||
'due_note' => '11月',
|
||||
'status' => SystemizationTodo::STATUS_IN_PROGRESS,
|
||||
'status' => SystemizationTodo::STATUS_INCOMPLETE,
|
||||
'progress' => '等待平台确认',
|
||||
'notes' => '',
|
||||
])->assertRedirect();
|
||||
@ -407,6 +416,92 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
$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,
|
||||
'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('收入', $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',
|
||||
]);
|
||||
$legacy = $this->createTodo('收入', '提醒测试', '历史进行中事项', '2026-11-01', 5);
|
||||
$legacy->update(['status' => '进行中']);
|
||||
|
||||
$content = $this->get(route('business.pages.show', [
|
||||
'section' => 'other',
|
||||
'page' => 'systemization-todos',
|
||||
]))->assertOk()->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(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());
|
||||
} finally {
|
||||
Carbon::setTestNow();
|
||||
}
|
||||
}
|
||||
|
||||
public function test_page_filters_by_date_and_merges_adjacent_category_and_section_cells(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
@ -423,7 +518,9 @@ 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('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)
|
||||
@ -438,7 +535,7 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
$this->assertContains('平台收入核对', $pageContent['todoContentOptions']['收入']);
|
||||
$this->assertArrayNotHasKey('1、收入', $pageContent['todoContentOptions']);
|
||||
$this->assertSame(
|
||||
['__overdue__', '待办'],
|
||||
['__overdue__', SystemizationTodo::STATUS_INCOMPLETE, SystemizationTodo::STATUS_COMPLETED],
|
||||
array_column($pageContent['todoStatusOptions']->all(), 'value'),
|
||||
);
|
||||
$pageItems = $pageContent['items'];
|
||||
@ -562,7 +659,7 @@ class SystemizationTodosTest extends FeatureTestCase
|
||||
->all(),
|
||||
);
|
||||
$this->assertSame(
|
||||
[$completed->id, $inProgress->id],
|
||||
[$pending->id, $completed->id, $inProgress->id],
|
||||
$items
|
||||
->where('type_sequence', 1)
|
||||
->where('content_sequence', 1)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user